Best practices for SaaS apps
App connectors are a powerful concept that help apps perform route discovery and implement IP allowlisting on upstream firewalls, so only traffic you trust can get to that app. This is useful for internal applications on ephemeral IP space, SaaS apps, and broadly any public resources on which your organization cannot install Tailscale.
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.
In Tailscale v1.59.43 and later, app connectors immediately advertise all discovered IPs for a single domain resolution as a single route change, not one per route. This change greatly cuts the number and frequency of disruptions during these rollouts.
To reduce reconfiguration time during discovery, we recommend you choose one of the following strategies:
- Use preconfigurations and ACL-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 allow unrelated traffic. It could include traffic to other content on those same public IP addresses through your app connectors. As a side effect, this can cause high bandwidth through those connectors for sites unrelated to the target app.
We recommend avoiding forcing CDN and multi-tenant content through the app connector.
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 pre-configurations.
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.
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.
The main downside of deferred approval is that users can't access their apps until the routes are approved.
Preconfiguration
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.
- Configure known subdomains when configuring apps.
Even in cases where deferred approval is possible, preconfiguration can shorten (or entirely remove) 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 ACL 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 ACL 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)
}