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.

This feature is currently in beta. To try it, follow the steps below to enable it for your network using Tailscale v1.54 or later.
App connectors are available for all plans.

Reduce reconfiguration during route discovery

One common problem with app connector deployments in environments with strict web application firewall configurations is that route discovery can be disruptive as newly discovered and advertised routes create network routing changes that cause disruptions for some route-aware software (most notably Google Chrome).

In client version 1.59.43 and later, app connectors immediately advertise all discovered IP addresses for a single domain resolution as a single route advertisement change rather than one advertisement per route. This change substantially reduces the number and frequency of disruptions during these more difficult 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 for serving static assets (for example, *.static.example.com or *.assets.acme.com).

Publishing these routes to your app connectors might also force unrelated traffic (such as traffic to other content on those same public IPs) through your app connectors. As a side effect, this can cause higher-than-expected bandwidth through those connectors for sites unrelated to the target app.

We recommend not forcing CDN and multi-tenant content through the app connector, where possible.

Connect large SaaS services

For customers with many domains or highly diverse domains with many IP endpoints, incremental discovery and publication of routes in the Tailscale App connector can cause periods of disruption as each newly advertised route causes client-level networking changes. Browsers like Google Chrome may raise errors like ERR_NETWORK_CHANGED, which can break down employee workflows.

As a high level and general recommendation, customers in these scenarios may want to consider deferring the enablement of WAF features on upstream providers until their initial connector deployments are well populated with routes. There are several strategies users can use to reduce the frequency of these events.

Deferred approvals

The deferred approvals approach builds in a non-disruptive “learning” period for new apps. You can use deferred approvals by configuring the app and app connectors without performing route advertisement, approving routes, or implementing upstream IP firewalls until a representative business period has elapsed.

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

Initially deploy the app connectors without overlapping autoApprovers. This results in the connectors receiving DNS queries from the client fleet and configuring advertised routes. However, the routes will not be automatically approved, and traffic will flow as usual. 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 disadvantage of the deferred approval approach is that the user cannot enable the upstream WAF until the routes are approved because their clients won’t have access to their applications.

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
  • 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)
}