Get started - it's free!
Login
© 2025

Best practices for using app connectors

App connectors are available for all plans.

To ensure reliable performance of your app connectors, we recommend several best practices. Approaches like preconfiguring settings and using deferred approvals can streamline setup and reduce potential disruptions. Understanding how traffic routes, especially in complex or multi-tenant environments can also help prevent issues related to performance and unintended access to your applications.

Configure apps for route discovery

You can optimize route discovery and efficiency for your app connectors using domain names, CNAME records, and wildcards.

Domains

You can configure app connectors with a list of valid, fully qualified domain names (FQDNs). Tailscale routes DNS records like A for IPv4 addresses and AAAA for IPv6 addresses through app connectors.

App connectors automatically discover routes when you add a domain to their configuration. Without pre-configured routes, the app connector enters a learning period where it proxies DNS requests for configured domains from end-users. The app connector identifies routing records from these requests and advertises them as routes.

With auto approval enabled, newly discovered routes are automatically accepted by the tailnet and instantly advertised to devices. If the tailnet uses regional routing, it sends traffic through an app connector in the same region. The learning period ends when the app connector discovers all routes. This typically completes quickly for smaller domains, but may take longer for large domain lists or domains with wildcards.

DNS lookups

App connectors work by acting as the authoritative nameserver for the domains configured on an app. For example, when you configure an app for a domain (such as *.example.com), it creates an invisible split DNS entry. Tailscale sends any subsequent DNS lookups for *.example.com to the appropriate app connectors. Each app connector performs the DNS lookup. If the lookup succeeds, it introduces a new subnet route for the resolved IP address before returning the response to the device that initiated the DNS lookup.

CNAME records

App connectors support (CNAME) records by resolving them to their target addresses and automatically advertising the resulting routes. Since CNAMEs are DNS aliases (not routing records), the connector follows the chain and automatically advertise the resulting routes. A CNAME chain is a sequence of DNS records where each CNAME points to another domain name, until it resolves to an A record that indicates destination's IP address.

Use the dig command to verify CNAME configuration. The following code block contains an example of the expected output:

.1. app.acme.com IN CNAME acme.backend.com
.2. acme.backend.com A ...

In the above example, the target of the CNAME chain, acme.backend.com, will route advertisements instead of app.acme.com.

If a web service uses geographic DNS load balancing, CNAME records might return different results in different locations. In these cases, it's best practice to deploy your app connectors in locations that closely mirror your organization's global footprint, and to leverage Tailscale's regional routing.

Wildcards

Your app connector configuration can include wildcards (*). Wildcard behavior is non-inclusive of the parent domain. For example:

  • To include all of acme.com and its subdomains, use both acme.com and *.acme.com.
  • To only include the top-level domain, include acme.com and omit *.acme.com.
  • To include only subdomains, include *.acme.com and omit acme.com.

Access control policy-driven route configuration

You can manually pre-configure routes as access control policies in your tailnet policy file inside the nodeAttrs top-level field, including domain configuration. For example:

{
			"target": ["*"],
			"app": {
				"tailscale.com/app-connectors": [
					{
            "name": "example-app",
            "connectors": ["tag:example-connector"],
            "domains": ["example.com"],
            "routes": ["192.0.2.0/24"],
          }
				],
			},
		}

The app connector devices in your tailnet automatically advertise these routes as soon as you apply the access control policy change to the tailnet policy file. App connectors for the application will immediately advertise these routes, and routes added using the access control policy section of the tailnet policy file are implicitly approved and do not require a sibling autoApprovers entry.

This maintains strict routing control as part of the access control and approval process. In many instances, it also removes the need to use large --advertise-routes arguments for device configurations to avoid lengthy discovery periods with disruptive routing changes.

Route coalescing

Routes populated by access control policy-driven route configuration coalesce or replace routes that are a sub-route of the new advertisement. For example, if the app connector previously discovered 192.0.2.5/32 and 192.0.2.67/32, and you update the access control policy to include 192.0.2.0/24, the connector replaces both /32 routes with the single 192.0.2.0/24 entry.

Reduce reconfiguration during route discovery

App connector route discovery can be disruptive in environments with strict firewall configurations. In these environments, new routes can disrupt route-aware software like Google Chrome.

App connectors immediately advertise all discovered IPs for a single domain resolution as a single route change containing all newly discovered routes for that domain resolution.

To reduce reconfiguration time during discovery, we recommend you choose one of the following strategies:

  • Use preconfigurations and access control policy-driven route configurations as extensively as possible.
  • Use deferred approvals. With deferred approvals, you delay enabling app firewalls until you discover the routes in an unapproved state, then approve them all in batches.

Configure apps with multi-tenant IP space

Some apps use content delivery networks (CDNs) or shared IP space to serve static assets. For example, *.static.example.com or *.assets.acme.com.

Publishing these routes to your app connectors might force unrelated traffic through the app connector, as CDNs often share IPs and can include traffic to other content on those same public IP addresses. As a side effect, this can cause high bandwidth through those connectors for sites unrelated to the target app. Therefore, we recommend serving CDN and multi-tenant content outside app connectors.

Connect large SaaS services

New route advertisements trigger client network modifications that might raise errors. For example, Google Chrome might report errors like ERR_NETWORK_CHANGED. You can reduce the frequency of these events using deferred approvals or preconfigurations.

Deferred approvals

The deferred approvals approach builds in a non-disruptive learning period for new app connectors. You can defer approvals by temporarily disabling route approval or using upstream IP firewalls.

Defer route approval until after a representative business cycle has elapsed.

Install app connectors separately to prevent overlapping autoApprovers. The app connectors receive DNS queries from the client fleet and configure advertised routes. But routes remain unapproved, and traffic maintains its regular pattern. Running in this state for a representative sample of business operations (such as a week) collects the bulk of affected routes. You can then transfer the routes to an autoApprovers clause.

Deferred approval blocks users from accessing an application until you approve the routes.

Preconfiguration

Configure routes while setting up connectors

You can preconfigure apps if you can't run apps in a non-disruptive learning period long enough to collect all the affected routes for the deferred approvals approach. We recommend you configure routes when setting up app connectors and configure known subdomains when configuring applications.

Even in cases where deferred approval is possible, preconfiguration can reduce the learning phase.

Routes

Many services, such as GitHub and Okta, publish IP address lists that you can collect into --advertise-routes preferences for the app connectors. Preconfiguring routes optimizes them in the following ways:

  • It reduces the routing table size and costs by using address ranges rather than single IP addresses.
  • You can enable upstream WAF immediately because most service-used routes are configured at boot.

Domains

Many applications have wildcard domains that the app connector cannot pre-flight because it does not know all the subdomains it needs to resolve. If you know all the subdomains, you can explicitly add them alongside the wildcard domains to generate a list of preconfigured routes.

Tip: You can gather subdomains from browser histories, DNS server histories, and DNS logs.

Automate preconfiguration

You can automate preconfiguration with a tool Tailscale offers called connector-gen. It provides a simple example of parsing some common providers and producing tailnet policy file snippets and --advertise-routes flags that you can use to manually perform the preconfiguration of routes strategy.

To run the connector-gen tool:

 ./tool/go run ./cmd/connector-gen github

The example above produces snippets of JSON for the access control policy and an --advertise-routes flag.

Automation with Terraform

Terraform has an HTTP data provider that you can use to consume IP address lists from upstream providers. You can then use the IP address lists to preconfigure routes when you deploy an app connector.

The following example shows preconfigurations for services like Okta, GitHub, and Amazon Web Services (AWS).

data "http" "okta_ip_range_json" {
  url = "https://s3.amazonaws.com/okta-ip-ranges/ip_ranges.json"
}

locals {
  okta_ip_range_data = jsondecode(data.http.okta_ip_range_json.response_body)
  okta_cell_ranges   = [for key in keys(local.okta_ip_range_data) : local.okta_ip_range_data["${key}"].ip_ranges if key == "us_cell_1"]
  okta_ip_ranges     = sort(distinct(flatten(local.okta_cell_ranges)))
}

data "http", "github_meta_json" {
  url = "https://api.github.com/meta"
}

locals {
  github_meta = jsondecode(data.http.github_meta_json.response_body)
  github_ips  = concat(local.github_meta.hooks, local.github_meta.web, local.github_meta.api, local.github_meta.git, local.github_meta.github_enterprise_importer, local.github.meta.packages, local.github_meta.pages, local.github_meta.importer, local.github_meta.actions, local.github_meta.dependabot)
  github_domains = concat(local.github_meta.domains.website, local.github_meta.domains.codespaces, local.github_meta.domains.copilot, local.github_meta.domains.packages, local.github_meta.domains.actions)
}

data "http", "aws_ip_ranges_json" {
  url = "https://ip-ranges.amazonaws.com/ip-ranges.json"
}

locals {
  aws_ip_range_data = jsondecode(data.http.aws_ip_ranges_json.response_body)
  aws_ip_ranges     = sort(distinct(concat([for prefix in local.aws_ip_range_data.prefixes : prefix.ip_prefix], [for prefix in local.aws_ip_range_data.ipv6_prefixes : prefix.ipv6_prefix])))
}

locals {
  all_ip_ranges = conact(local.okta_ip_ranges, local.github_ips, local.aws_ip_ranges)
}

Last updated Jun 3, 2025