> For the complete documentation index, see [llms.txt](https://docs.p0.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.p0.dev/integrations/resource-integrations/tailscale.md).

# Tailscale

Install P0 for a tailnet so people can request just-in-time network access through the P0 postures your policy file defines.

{% hint style="info" %}
The Tailscale integration is in beta. Functionality and configuration may change.
{% endhint %}

{% hint style="info" %}
**Requirements:** the Tailscale integration needs a Tailscale Premium or Enterprise plan, which has the custom device posture attributes that P0 sets.
{% endhint %}

With P0 installed for a tailnet, people can request a P0 posture on one of their own devices. Your policy file decides what a device with that posture can reach. P0 sets the posture for the approved duration, then takes it away.

## How it works

* **People request a P0 posture.** A request names a posture that your policy file defines, such as `posture:p0_prod_db`, and the requestor's device to set it on. The list of postures shows what the tailnet's policy file has right now. Your access policies decide who can request what. See [#access-policies](#access-policies "mention").
* **Your policy file gates grants on P0 postures, and P0 never edits it.** Your own grants require a P0 posture with `srcPosture`, and P0 sets that posture's device attribute on the requestor's device. Because P0 only reads the policy file, it works the same whether you edit it in the admin console or manage it as code, such as with a GitOps pipeline or Tailscale's `tailscale_acl` Terraform resource. See [#set-up-postures-in-your-policy-file](#set-up-postures-in-your-policy-file "mention").
* **You deploy a connector in your cloud.** It runs on AWS Lambda or Google Cloud Run, from Terraform that the installer generates, and calls only the Tailscale API. One connector can serve many tailnets.
* **The connector signs in to Tailscale with a credential you create.** By default it's a Tailscale federated identity that trusts the connector's own cloud identity, so no secret is stored anywhere. You can use an OAuth client instead. Its secret stays in your AWS Secrets Manager or Google Secret Manager, where the connector reads it. P0 never does.
* **P0 matches people by email.** A requestor must be a member of the tailnet, signed in to Tailscale with the email they use for P0. P0 never adds or removes tailnet members.
* **P0 checks a request before anyone approves it.** The requested posture must be in your policy file, and the device must be the requestor's own. Approvers see the tailnet by its Tailnet ID and DNS name, such as `T1234CNTRL (tail1234.ts.net)`. They also see what the posture unlocks in your policy file: each grant, legacy `acls` entry and Tailscale SSH rule it gates, and the Kubernetes groups a grant gives. See [#set-up-postures-in-your-policy-file](#set-up-postures-in-your-policy-file "mention").
* **Every grant has an end.** P0 refuses Tailscale access with no duration.

For what a requestor gets, see [Requesting Tailscale access](/integrations/resource-integrations/tailscale/requesting-access.md).

## Set up postures in your policy file

You add the P0 postures, and the grants they gate, to your policy file. If you manage the policy file as code, add them there. Your next push or apply leaves P0's access in place.

For each posture people may request, define a P0 posture, `posture:p0_<name>`, with the assertion `custom:p0_<name> == true`, and require it in your grants with `srcPosture`. P0 refuses a request for a posture that lacks that assertion, because a device could pass the posture without P0. When a request is approved, P0 sets the attribute to `true` on the requestor's chosen device, and removes it when access ends. The attribute also expires when the grant ends, so Tailscale removes it then even if P0's revoke fails. `<name>` takes lowercase letters, digits and underscores. It can't be `install_check`, which P0 keeps for its install check.

For example, this lets a device reach `tag:prod-db` on port 5432 only while P0 has given it the posture `posture:p0_prod_db`:

```json
"postures": {
  "posture:p0_prod_db": {"assertions": ["custom:p0_prod_db == true"]}
},
"grants": [
  {
    "src": ["autogroup:member"],
    "dst": ["tag:prod-db"],
    "ip": ["tcp:5432"],
    "srcPosture": ["posture:p0_prod_db"]
  }
]
```

A posture can also be a plain list of assertions, such as `"posture:p0_prod_db": ["custom:p0_prod_db == true"]`.

Several postures in one `srcPosture` let in a device with any of them. A grant that names its own `srcPosture` no longer gets your `defaultSrcPosture`, so repeat any default posture you rely on. An entry whose own `srcPosture` is missing, `null` or `[]` takes your `defaultSrcPosture`.

Another posture of yours that reads `custom:p0_<name>` can also pass on a device while P0 has set the attribute. So when someone requests the posture, approvers see what those postures gate too, each entry with `via <posture>`. That includes entries that take a posture as your `defaultSrcPosture`, and entries in legacy `acls` as well as in `grants` and `ssh`.

If an entry also lists a posture that doesn't read the attribute, a device can reach it through that posture without P0. Approvers see those postures after the entry, as `(also open through <postures> without P0)`. For example: `posture:p0_db: tag:db tcp:5432 for group:eng (also open through posture:linux without P0)`.

P0 manages every device attribute whose name starts with `custom:p0_`. Don't set those attributes yourself. Your other device attributes stay as they are.

Postures also need:

* **At least one untagged device the tailnet owns when you add it.** Before the tailnet is installed, P0 checks that it may set device attributes by setting `custom:p0_install_check` on one such device and removing it at once. P0's later checks of an installed tailnet set no attribute.
* **The requestor's own device.** P0 sets a posture only on an untagged device signed in as the requestor. A tagged device belongs to its tags, so P0 refuses it. A device shared in from another tailnet isn't the tailnet's own, so P0 refuses it too, and doesn't offer it in the device list.

## Before you begin

You need:

* **A tailnet, by its Tailnet ID**, such as `T1234CNTRL`, from **Settings** > **General** in the Tailscale admin console. The tailnet's DNS name, such as `tail1234.ts.net`, can't be used.
* **Someone who can create trust credentials in the tailnet**, such as an Owner, Admin or Network admin.
* **A tailnet member for each requestor, with their P0 email.** P0 sets a posture only on an untagged device signed in to the tailnet with the requestor's P0 email.
* **One P0 organization for the tailnet.** Add a tailnet to one P0 organization only. Two organizations that act on the same tailnet remove each other's access.
* **A Tailscale Premium or Enterprise plan**, and at least one untagged device the tailnet owns.
* [**Terraform**](https://developer.hashicorp.com/terraform/install).

The connector runs on AWS Lambda or Google Cloud Run. An OAuth client's secret goes in the same cloud's secret manager, in any account or project there.

For a connector on AWS Lambda, you also need:

* **AWS IAM management for the connector's account.** See [AWS](/integrations/resource-integrations/aws.md#setting-up-aws-iam-management).
* **For an OAuth client, AWS Secrets Manager integration for the account that holds its secret.** See [AWS Secrets Manager](/integrations/resource-integrations/aws-secrets-manager.md).
* **Terraform 1.4 or later, run where Docker and the AWS CLI are installed**, with the AWS CLI signed in to the connector's account.
* **Commercial AWS regions** for the connector and any secret. GovCloud, China, ISO and European Sovereign Cloud regions aren't supported.

For a connector on Google Cloud Run, you also need:

* **Google Cloud IAM management for the connector's project.** See [Google Cloud](/integrations/resource-integrations/google-cloud.md#install-iam-management-on-each-project).
* **For an OAuth client, Google Secret Manager integration for the project that holds its secret.** See [Google Secret Manager](/integrations/resource-integrations/google-secret-manager.md).
* **The IAM, Cloud Run and Secret Manager APIs** enabled in the connector's project: `iam.googleapis.com`, `run.googleapis.com` and `secretmanager.googleapis.com`.
* **The `gcloud` CLI.**

## Install Tailscale

1. Open the P0 dashboard at [p0.app](https://p0.app) and log in to your organization.
2. In the sidebar, click **Integrations**. Under **Resources**, click **Tailscale**.

You install the **Connector** component in steps 1 and 2, usually once. You install the **Tailnet** component in steps 3 to 6, once for each tailnet.

### Step 1: Add a connector

You can't change where a connector runs once you add it.

1. Select the **Connector** component, and click **Add connector**. The form is titled **Installing a new connector**.
2. In **Connector ID**, enter a name for the connector, such as `prod`. Use lowercase letters, digits and hyphens, up to 63 characters, starting and ending with a letter or digit.
3. In **Connector hosting**, choose **AWS Lambda** or **Google Cloud Run**, and fill in its fields.

| Hosting              | Field                | What to enter                                                                                                                                                                   |
| -------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **AWS Lambda**       | **AWS account ID**   | The account the connector runs in. The list shows the accounts where P0's AWS IAM management is installed.                                                                      |
|                      | **Connector name**   | The name of the connector's Lambda function and IAM role. Use 2 to 64 lowercase letters, digits and single hyphens, starting with a letter and ending with a letter or digit.   |
|                      | **Connector region** | An AWS region, such as `us-west-2`                                                                                                                                              |
| **Google Cloud Run** | **GCP project ID**   | The project the connector runs in. The list shows the projects where P0's Google Cloud IAM management is installed.                                                             |
|                      | **Connector name**   | The name of the connector's Cloud Run service and service account. Use 6 to 30 lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit. |
|                      | **Connector region** | A Google Cloud region, such as `us-west1`                                                                                                                                       |

Until you install P0's Google Cloud IAM management, you can't choose **Google Cloud Run**, and P0 says: `P0 invokes a connector on Cloud Run as the service account of its Google Cloud integration, which isn't installed. Install P0's Google Cloud IAM management for the connector's project first.`

4. Click **Next**. P0 saves the connector and opens its setup page.

If P0 can't save the connector, it says why under **Error configuring Tailscale**. See [#when-you-add-a-connector-or-a-tailnet](#when-you-add-a-connector-or-a-tailnet "mention").

### Step 2: Deploy the connector

The setup page's Terraform deploys the connector where step 1 says, and lets P0 invoke it. On AWS, it also turns on the account's outbound identity federation, which lets the connector sign in to Tailscale with a federated identity. The connector needs no VPC. See [#what-the-connectors-deployment-creates](#what-the-connectors-deployment-creates "mention").

1. Copy the Terraform, and add it to your configuration. Leave the account or project, the region and the connector name as they are.
2. For a connector on AWS Lambda, before you apply it:
   * If your configuration already has a `required_providers` block, move the snippet's `aws` and `docker` providers into it and delete the snippet's block.
   * Add the credential settings your other AWS providers use, such as `profile` or `assume_role`, to the snippet's `aws` provider.
   * Check whether outbound identity federation is already on for the account, with AWS CLI 2.32.0 or later: `aws iam get-outbound-web-identity-federation-info`. If it's on, import the snippet's `aws_iam_outbound_web_identity_federation` resource instead of creating it, with the `terraform import` command in the snippet's comment.
3. Apply it.
4. Click **Next**. P0 checks that it can invoke the connector. If the check fails right after you apply, wait a minute and click **Next** again. See [#when-p0-validates-the-install](#when-p0-validates-the-install "mention").
5. On the configure page, click **Finish**. Tailnets can pick the connector only after you do.

{% hint style="warning" %}
**AWS outbound identity federation is a setting of the whole AWS account.** Destroying the connector's Terraform turns it off for everything in the account. To remove the connector and leave federation on, first replace the `aws_iam_outbound_web_identity_federation` resource with the `removed` block that the snippet's comment shows, which needs Terraform 1.7 or later.
{% endhint %}

### Step 3: Create the Tailscale credential

You create the credential first, because the tailnet's form asks for its client ID.

1. On the Tailscale integration's page, select the **Tailnet** component, and click **Add tailnet**. The form is titled **Installing a new tailnet**. Its top lists the scopes, and the Terraform for a federated identity for each of your connectors.
2. Create a federated identity or an OAuth client, as below, with exactly these scopes:

| Scope                        | Why the connector needs it                                                                                                                                                                                                             |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `policy_file:read`           | To list the P0 postures people can request, to check that a requested posture exists and asserts its `custom:p0_` attribute, and to show approvers what the posture unlocks                                                            |
| `users:read`                 | P0's install check reads the tailnet's members to confirm the tailnet exists. P0 matches a requestor to their devices, not to this list.                                                                                               |
| `devices:core:read`          | To list a requestor's own untagged devices, to check at grant that the device is theirs and untagged, to tell at revoke whether a device has left the tailnet, and to find the tailnet's DNS name and the device for the install check |
| `devices:posture_attributes` | The connector's only write. It sets and removes a posture's `custom:p0_` attribute at grant and revoke, and `custom:p0_install_check` during the install check. It also reads a device's attributes, so it changes only P0's.          |
| `devices:routes:read`        | P0's install check reads one device's routes. No request, grant or revoke reads them.                                                                                                                                                  |
| `services:read`              | P0's install check lists the tailnet's services. No request, grant or revoke reads them.                                                                                                                                               |

Tailscale adds `devices:posture_attributes:read` to a credential with `devices:posture_attributes`, so the admin console shows seven scopes. P0's check also accepts `all:read` in place of every `:read` scope, but give the credential only the scopes above. P0 refuses a credential that has `policy_file` or `all`, because either can write the policy file, which P0 never does. No scope above can write the policy file, change a tailnet member or add a device.

**A federated identity is the default.** It trusts the connector's own cloud identity, so no secret is stored anywhere, and it needs no tags. To create it with Tailscale's Terraform provider, expand **Show the Terraform for a federated identity that trusts connector `<connector ID>`** on the form, copy it, and apply it with the `tailscale` provider signed in to the tailnet you're adding. Sign the provider in with a credential of your own that may create trust credentials, such as with the `TAILSCALE_OAUTH_CLIENT_ID` and `TAILSCALE_OAUTH_CLIENT_SECRET` environment variables. The Terraform outputs the client ID to enter in the form.

For a connector on AWS Lambda, the issuer is the connector's account's outbound identity federation, and the subject is the connector's IAM role, which has the connector's name:

```terraform
data "aws_iam_outbound_web_identity_federation" "p0" {}

resource "tailscale_federated_identity" "p0" {
  description = "P0 connector prod"
  issuer      = data.aws_iam_outbound_web_identity_federation.p0.issuer_identifier
  subject     = "arn:aws:iam::123456789012:role/p0-tailscale-prod"
  scopes = [
    "policy_file:read",
    "users:read",
    "devices:core:read",
    "devices:posture_attributes",
    "devices:routes:read",
    "services:read",
  ]
}
```

For a connector on Google Cloud Run, the issuer is Google, and the subject is the numeric unique ID of the connector's service account, not its email:

```terraform
data "google_service_account" "p0_tailscale_connector" {
  project    = "my-project-id"
  account_id = "p0-tailscale-prod"
}

resource "tailscale_federated_identity" "p0" {
  description = "P0 connector prod"
  issuer      = "https://accounts.google.com"
  subject     = data.google_service_account.p0_tailscale_connector.unique_id
  scopes = [
    "policy_file:read",
    "users:read",
    "devices:core:read",
    "devices:posture_attributes",
    "devices:routes:read",
    "services:read",
  ]
}
```

Leave `audience` unset, so that Tailscale uses `api.tailscale.com/<client ID>`, which is the audience the connector asks for. You can also create the federated identity in the Tailscale admin console, under **Settings** > **Trust credentials**, with the issuer, subject and scopes the Terraform names.

**An OAuth client is the alternative**, for a tailnet where you can't use a federated identity. Create it in the Tailscale admin console under **Settings** > **Trust credentials**, with exactly the scopes in step 2 and no tags. Copy its client ID for step 4. Copy its secret too, which Tailscale shows only once, and keep it for step 5. You store the secret in your secret manager yourself, so it never reaches P0 or your Terraform state.

### Step 4: Add the tailnet

Once you add a tailnet, you can't change its connector or its credential.

1. In the form from step 3, in **Tailnet ID**, enter the tailnet's ID, such as `T1234CNTRL`.
2. In **Connector**, pick the connector that acts on this tailnet. If you haven't finished one, P0 says `Install a Tailscale connector first.` Go back to steps 1 and 2.
3. In **Credential**, choose **Federated identity** or **OAuth client**. In **Client ID**, enter the client ID from step 3, such as `kAbc1CNTRL`.
4. For an OAuth client, in **Secret manager provider**, choose the secret manager in the connector's cloud, and fill in its fields:

| Secret manager provider                                 | Field              | What to enter                                                                                                                     |
| ------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| **AWS Secrets Manager**, for a connector on AWS Lambda  | **Installation**   | The AWS account the secret is in. The list shows the accounts where P0's AWS Secrets Manager integration is installed.            |
|                                                         | **Secrets region** | The secret's AWS region, such as `us-west-2`                                                                                      |
| **Google Secret Manager**, for a connector on Cloud Run | **Installation**   | The Google Cloud project the secret is in. The list shows the projects where P0's Google Secret Manager integration is installed. |

5. For an OAuth client, in **Client secret's secret**, enter the name of the secret that holds, or will hold, the OAuth client's secret. Enter the name, not the ARN or the full resource name, and never the client's secret itself. In AWS Secrets Manager, the name can't end in a hyphen and six letters or digits, such as `-AbCdEf`.
6. Click **Next**. P0 saves the tailnet and opens its setup page.

If an OAuth client's secret is in another AWS account than the connector, encrypt it with a customer-managed KMS key. Another account can't use the default `aws/secretsmanager` key.

### Step 5: Finish the tailnet's setup

The setup page lists the tailnet's remaining steps, with your tailnet's values filled in. What's left depends on its credential.

**For a federated identity**, the credential needs nothing more. The setup page's **Deploy the connector** and **Create the federated identity** steps repeat steps 2 and 3. The federated identity's client ID must be the one you entered.

**For an OAuth client**, the setup page's **Create the OAuth client** step repeats step 3. Then store the client's secret, and let the connector read it:

1. Run this, paste the OAuth client's secret and press Enter. It reads the secret without showing it:

```bash
read -rs TAILSCALE_CLIENT_SECRET
```

2. Create the secret. In AWS Secrets Manager:

```bash
printf %s "$TAILSCALE_CLIENT_SECRET" | aws secretsmanager create-secret \
  --name <secret-name> \
  --secret-string file:///dev/stdin \
  --region <region>
```

In Google Secret Manager:

```bash
printf %s "$TAILSCALE_CLIENT_SECRET" | gcloud secrets create <secret-name> \
  --project <project-id> \
  --data-file=-
```

For a secret in another AWS account, the setup page's command also passes `--kms-key-id <kms-key-arn>`. Replace `<kms-key-arn>` with the key's ARN. If the secret already exists, store the client's secret as a new version instead, as in [#change-the-install-later](#change-the-install-later "mention").

3. Clear the secret from your shell:

```bash
unset TAILSCALE_CLIENT_SECRET
```

4. Copy the Terraform that lets the connector read the secret, add it to your configuration, and apply it. On AWS, add your usual credential settings to its `aws` provider, and if a customer-managed KMS key encrypts the secret, set `kms_key_arn` to the key's ARN.

The Terraform lets the connector read this one secret, and nothing else:

| Secret manager        | What the Terraform grants                                                                                                                                                                                                                                                                                                                                                                           |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AWS Secrets Manager   | A policy on the connector's role, `ReadTailscaleOAuthClientSecret-<tailnet>`, with `secretsmanager:GetSecretValue` on the secret, to read the OAuth client's secret and sign in to Tailscale. If you set `kms_key_arn`, it also has `kms:Decrypt` on that key, through Secrets Manager only, to decrypt the secret.                                                                                 |
| Google Secret Manager | `roles/secretmanager.secretAccessor` for the connector's service account, to read the OAuth client's secret and sign in to Tailscale, and `roles/secretmanager.viewer`, to read the secret version it reads from, which the accessor role doesn't allow. Both are on the secret only. Each adds one member to the secret's IAM policy and replaces nothing. The secret must exist before you apply. |

**If the secret is in another AWS account than the connector**, the setup page has two configurations:

1. Apply the first in the connector's account. Terraform won't plan it until you set `kms_key_arn`.
2. Apply the second in the secret's account, after the connector's own Terraform. It sets the secret's resource policy so the connector's role can read it, and outputs a statement to add to the KMS key's policy, with `kms:Decrypt` through Secrets Manager only, so the role can decrypt it.

{% hint style="warning" %}
The second configuration replaces the secret's whole resource policy. If the secret already has one, leave the `aws_secretsmanager_secret_policy` resource out, and add its statement to your policy instead.
{% endhint %}

{% hint style="warning" %}
**Limit the connector's read access to the secrets of the tailnets it serves.** P0 names the secret in each call to the connector, so a call could point it at any other secret it can read.
{% endhint %}

**For either credential**, the setup page's **Gate grants on P0 postures** step shows a posture snippet. Add a P0 posture for each set of grants people may request, and gate those grants on it, as in [#set-up-postures-in-your-policy-file](#set-up-postures-in-your-policy-file "mention"). When someone requests a posture, P0 checks that it exists and asserts `custom:p0_<name> == true`.

### Step 6: Validate

1. Click **Next**. P0 asks the connector to read the OAuth client's secret or get a token for its own cloud identity, sign in to Tailscale, and check that the credential has every scope and none that can write the policy file. It also checks that it may set device attributes. Then it counts the P0 postures in the policy file.
2. On the configure page, check **Discovered** and **DNS name**. **Discovered** counts the P0 postures in the tailnet's policy file. **DNS name** is the tailnet's DNS name, such as `tail1234.ts.net`, which requestors and approvers see next to the Tailnet ID. P0 reads it from the tailnet's own devices, so it's empty for a tailnet with none.
3. Click **Finish**. People can't request access on the tailnet until you do.

If a check fails, P0 says what to fix under **Error configuring Tailscale**, and what it checked. See [#when-p0-validates-the-install](#when-p0-validates-the-install "mention"). If the connector is denied right after you apply the Terraform, wait a few seconds and click **Next** again.

{% hint style="success" %}
People can now request access on the tailnet. They pick a posture from the ones your policy file defines, and your access policies decide who can request what. See [Requesting Tailscale access](/integrations/resource-integrations/tailscale/requesting-access.md) and [#access-policies](#access-policies "mention").
{% endhint %}

## What the connector's deployment creates

The connector's Terraform gives the connector no Tailscale credential. Each tailnet's federated identity trusts the connector's identity, or the tailnet's Terraform lets the connector read one OAuth client's secret, as in [#step-5-finish-the-tailnets-setup](#step-5-finish-the-tailnets-setup "mention").

On AWS Lambda:

| Resource                                                       | What it's for                                                                                                                                                                                                                                                                                 |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| An ECR repository                                              | Holds the copy of P0's connector image that Lambda runs, with immutable tags                                                                                                                                                                                                                  |
| A CloudWatch log group, `/aws/lambda/<connector-name>`         | The connector's logs, kept for 30 days. Change `log_retention_days` to keep them longer or shorter.                                                                                                                                                                                           |
| An IAM role, named after the connector                         | The role the connector runs as, and that each tailnet's federated identity names as its subject. It has `logs:CreateLogStream` and `logs:PutLogEvents` on its own log group, so the connector writes its own logs and no others.                                                              |
| AWS outbound identity federation for the account               | Lets the connector's role mint the identity tokens that a federated identity trusts                                                                                                                                                                                                           |
| A policy on the connector's role, `MintTailscaleIdentityToken` | `sts:GetWebIdentityToken` for Tailscale's audiences, `api.tailscale.com/*`, for tokens that last 300 seconds or less. The connector trades the token for a Tailscale token in place of a secret. The audience allows any client ID, because the connector also serves tailnets you add later. |
| The Lambda function                                            | Runs the connector, with no VPC                                                                                                                                                                                                                                                               |
| A policy on P0's role in the account                           | `lambda:InvokeFunction` on the connector's function, so P0 can call the connector, and nothing else                                                                                                                                                                                           |

On Google Cloud Run:

| Resource                                                  | What it's for                                                                                                                                         |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| A service account, named after the connector              | The identity the connector runs as, and that each tailnet's federated identity names as its subject. The connector's Terraform gives it no IAM roles. |
| The Cloud Run service                                     | Runs the connector from P0's image on Docker Hub, with no VPC                                                                                         |
| Two IAM bindings on the service, for P0's service account | `roles/run.invoker`, to invoke the connector, and `roles/run.viewer`, to read the service's URL when you install                                      |

## Install with Terraform

{% hint style="info" %}
The `p0_tailscale_*` resources require P0 Terraform provider [v0.86.0](https://github.com/p0-security/terraform-provider-p0/releases/tag/v0.86.0) or later.
{% endhint %}

Instead of the installer, you can install Tailscale with the P0 Terraform provider. See [Manage integrations as code with the P0 Terraform provider](/getting-started/get-started-with-the-p0-terraform-provider.md). In the Terraform registry, the pages of `p0_tailscale_connector` and `p0_tailscale_tailnet` have complete examples for AWS Lambda and Google Cloud Run, including the cloud and Tailscale resources, and the tailnet's page has one for an OAuth client too. The examples go in one configuration: the connector's AWS Lambda example holds its `required_providers` block, `tailscale/tailscale` included. Create the resources in this order:

1. **Stage the connector** with [`p0_tailscale_connector_staged`](https://registry.terraform.io/providers/p0-security/p0/latest/docs/resources/tailscale_connector_staged). Set `id` and `hosting`. P0 answers with `connector_identity`, the IAM role or service account the connector must run as.
2. **Deploy the connector** where `hosting` says. Pin the connector's image to a published tag and that tag's digest, as the examples do. P0 publishes the tags on Docker Hub.
   * **AWS Lambda:** use the image `p0security/p0-connector-tailscale`, copied into Amazon ECR. Run the function as `connector_identity.role_name`, and give P0's AWS IAM management role `lambda:InvokeFunction` on the function. Turn on AWS outbound identity federation for the account, and give the connector's role the `MintTailscaleIdentityToken` policy: `sts:GetWebIdentityToken` for Tailscale's audiences, `api.tailscale.com/*`, for tokens that last 300 seconds or less.
   * **Google Cloud Run:** use the image `p0security/p0-connector-tailscale-gcloud`, running as `connector_identity.service_account_email`. Give P0's Google Cloud service account `roles/run.invoker` and `roles/run.viewer` on the service, and set the connector's `INVOKER_SA_EMAIL` environment variable to that same service account's email.
3. **Install the connector** with [`p0_tailscale_connector`](https://registry.terraform.io/providers/p0-security/p0/latest/docs/resources/tailscale_connector), with the same `id` and `hosting` as the staged resource. P0 checks that it can invoke the connector. Destroying the connector, or changing its `id` or `hosting`, revokes nothing on its tailnets, so revoke their grants first. See [#change-the-install-later](#change-the-install-later "mention").
4. **Create the tailnet's credential**, as in [#step-3-create-the-tailscale-credential](#step-3-create-the-tailscale-credential "mention"). For an OAuth client, also store its secret and let the connector read it, as in [#step-5-finish-the-tailnets-setup](#step-5-finish-the-tailnets-setup "mention").
5. **Add each tailnet** with [`p0_tailscale_tailnet`](https://registry.terraform.io/providers/p0-security/p0/latest/docs/resources/tailscale_tailnet). Set `id` to the Tailnet ID, `connector` and `credential`, and make it depend on the credential's IAM resources. P0 validates the tailnet when Terraform creates it. Terraform refuses a change to an installed tailnet's `connector` or `credential`. Destroying the resource, or changing its `id`, removes the tailnet from P0 without revoking its grants, so revoke them first. The `discovery` and `dns_name` attributes show what P0 found.
6. **Gate your grants on P0 postures** in your policy file, as in [#set-up-postures-in-your-policy-file](#set-up-postures-in-your-policy-file "mention"). If `tailscale_acl` manages the policy file, add the postures and grants there.

People then request the postures your policy file defines, so there's nothing more to create in P0. For example, a tailnet with a federated identity, on a connector that runs on AWS Lambda:

```terraform
resource "p0_tailscale_tailnet" "example" {
  id        = "T1234CNTRL"
  connector = p0_tailscale_connector.example.id

  credential = {
    type      = "federated"
    client_id = tailscale_federated_identity.p0.id
  }
}
```

## Access policies

People can request every P0 posture that a tailnet's policy file defines. Your access policies decide who can request which.

In access policies, Tailscale is the service `tailscale`, with the access type `posture`. See [Requesting Tailscale access](/integrations/resource-integrations/tailscale/requesting-access.md#what-you-can-request) and [Configuring access policies](/policy-studio/policy-studio/configure-your-first-access-policy.md#resource).

### Available filters

A rule can filter on these properties of a request, each with the key `name`:

| Filter    | What it matches       | Example              |
| --------- | --------------------- | -------------------- |
| `target`  | The requested posture | `posture:p0_prod_.*` |
| `tailnet` | The Tailnet ID        | `T1234CNTRL`         |

Patterns are unanchored, case-sensitive regular expressions. Use `^` and `$` to match a whole target. For example, `^posture:p0_prod_.*` matches every posture that starts with `posture:p0_prod_`.

### Example access policy

These policies send requests for postures that start with `posture:p0_prod_` to P0 approvers, and deny every posture on the tailnet `T5678CNTRL`:

```yaml
- requestor:
    type: any
  resource:
    type: integration
    service: tailscale
    accessType: posture
    filters:
      target:
        effect: keep
        key: name
        pattern: ^posture:p0_prod_.*
  approval:
    - type: p0
- requestor:
    type: any
  resource:
    type: integration
    service: tailscale
    accessType: posture
    filters:
      tailnet:
        effect: keep
        key: name
        pattern: ^T5678CNTRL$
  approval:
    - type: deny
```

## Error messages

Once you've fixed the cause of an error, retry what failed.

### When you add a connector or a tailnet

P0 checks the form when you click **Next** in [#step-1-add-a-connector](#step-1-add-a-connector "mention") or [#step-4-add-the-tailnet](#step-4-add-the-tailnet "mention"), and saves nothing if a check fails. The message shows under **Error configuring Tailscale**.

| Message                                                                                                                                                                                                | What to do                                                                                                                                                                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `The connector <id> is already added. To change it, remove <id> and add it again.`, or the same message for a tailnet                                                                                  | Open it from the list, or choose another ID.                                                                                                                                                      |
| `Use lowercase letters, digits and hyphens only, starting and ending with a letter or digit, up to 63 characters.`                                                                                     | Fix the connector ID.                                                                                                                                                                             |
| `Use 2 to 64 characters for the connector name: lowercase letters, digits and single hyphens, starting with a letter and ending with a letter or digit.`                                               | Fix the AWS Lambda **Connector name**.                                                                                                                                                            |
| `Use 6 to 30 characters for the connector name: lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit. The connector's service account takes the same name.` | Fix the Cloud Run **Connector name**.                                                                                                                                                             |
| `Enter the AWS region the connector runs in, such as us-west-2.`, or a similar message for another account, project or region field                                                                    | Fill in the field it names.                                                                                                                                                                       |
| `P0's Tailscale integration supports only AWS's commercial regions, not GovCloud, China or other AWS partitions.`                                                                                      | Use a commercial AWS region for the connector and the secret.                                                                                                                                     |
| `Enter the Tailnet ID from Tailscale's Settings, General, such as T1234CNTRL. The tailnet's DNS name, such as tail1234.ts.net, can't be used: Tailscale's API doesn't take it.`                        | Enter the Tailnet ID.                                                                                                                                                                             |
| `Enter the client ID of the Tailscale federated identity or OAuth client, as Tailscale's Trust credentials page shows it.`                                                                             | Copy the client ID from **Settings** > **Trust credentials**.                                                                                                                                     |
| `That's a Tailscale secret, which starts with tskey-, not a client ID. Enter the client ID, as Tailscale's Trust credentials page shows it.`                                                           | Enter the client ID. You pasted a Tailscale secret, so rotate it in Tailscale.                                                                                                                    |
| `Pick the connector that acts on this tailnet. Install a Tailscale connector first if there's none.`                                                                                                   | Pick a connector, or finish steps 1 and 2 first.                                                                                                                                                  |
| `The OAuth client's secret must be in the connector's cloud: AWS Secrets Manager for a connector on AWS Lambda, or Google Secret Manager for one on Cloud Run.`                                        | Change **Secret manager provider**.                                                                                                                                                               |
| `Enter the name of the secret that holds, or will hold, the OAuth client's secret.`                                                                                                                    | Fill in **Client secret's secret**.                                                                                                                                                               |
| `That doesn't look like a secret name. Enter the name of the secret that holds the OAuth client's secret, not its ARN, its full resource name or the client secret itself.`                            | Enter the secret's name.                                                                                                                                                                          |
| `That's a Tailscale secret, which starts with tskey-, not a secret's name. Put the OAuth client's secret in your secret manager, and enter the name of the secret that holds it.`                      | Enter the secret's name. You pasted the client's secret, so rotate it in Tailscale, and store the new one as in [#step-5-finish-the-tailnets-setup](#step-5-finish-the-tailnets-setup "mention"). |
| `Secrets Manager can't find a secret by its name when the name ends in a hyphen and six characters, like -AbCdEf. Use a secret whose name doesn't end that way.`                                       | Choose another secret name.                                                                                                                                                                       |

### When P0 validates the install

P0 validates a connector in [#step-2-deploy-the-connector](#step-2-deploy-the-connector "mention") and a tailnet in [#step-6-validate](#step-6-validate "mention"). A failed check shows under **Error configuring Tailscale**. In these messages, `<where it runs>` names the connector's function or service, such as `the Lambda function p0-tailscale-prod in AWS account 123456789012, region us-west-2`.

For a connector:

| Message                                                                                                                                                                       | What to do                                                                                 |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `P0 couldn't find <where it runs>. It may still be deploying. Check that the connector's Terraform applied, then retry.`                                                      | Check that the Terraform applied with the account or project, region and name from step 1. |
| `P0 isn't allowed to invoke <where it runs>. The connector's Terraform lets P0's role call lambda:InvokeFunction on the function: check that it applied, then retry.`         | Apply the connector's Terraform. Right after you apply it, wait a few seconds.             |
| `P0 isn't allowed to invoke <where it runs>. The connector's Terraform grants P0 roles/run.invoker on the service: check that it applied, then retry.`                        | Apply the connector's Terraform. Right after you apply it, wait a few seconds.             |
| `P0 isn't allowed to read <where it runs>, so it can't find its URL. The connector's Terraform grants P0 roles/run.viewer on the service: check that it applied, then retry.` | Apply the connector's Terraform.                                                           |
| `<where it runs> has no URL yet. It may still be deploying. Check that the connector's Terraform applied, then retry.`                                                        | Wait for the service to finish deploying.                                                  |
| `P0 found <where it runs>, but it isn't ready yet. It may still be starting. Retry in a minute, and check its logs if it isn't ready by then.`                                | Wait a minute. If it still isn't ready, check its logs.                                    |
| Any other message                                                                                                                                                             | Check the connector's logs.                                                                |

For a tailnet, a failure while P0 counts the P0 postures in the policy file ends with a note that says so:

| Message                                                                                                                                                                                                                                                                                                                                                | What to do                                                                                                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `The connector <id> isn't installed. Install it again with the same ID and hosting, or remove <tailnet> and add it again with another connector.`                                                                                                                                                                                                      | Add the connector again, as in steps 1 and 2, with the same ID and hosting.                                                                                                                                                                                                                  |
| `The connector couldn't find the Tailscale OAuth client secret <secret>. Check the client secret in the P0 install, and that the secret still exists.`                                                                                                                                                                                                 | Store the client's secret where the setup page shows, as in [#step-5-finish-the-tailnets-setup](#step-5-finish-the-tailnets-setup "mention"). If you named the wrong secret, remove the tailnet and add it again.                                                                            |
| `The connector isn't allowed to read the Tailscale OAuth client secret <secret>. Grant the connector's identity secretsmanager:GetSecretValue on that secret, and kms:Decrypt on its KMS key if it uses a customer-managed key.`, or the same message naming `secretmanager.versions.access` and `secretmanager.versions.get`                          | Apply the Terraform that lets the connector read the secret, as in step 5, with `kms_key_arn` if a customer-managed key encrypts the secret.                                                                                                                                                 |
| `The Tailscale OAuth client secret <secret> is empty. Store the Tailscale OAuth client's secret in it.`                                                                                                                                                                                                                                                | Store the client's secret as a new version, as in [#change-the-install-later](#change-the-install-later "mention").                                                                                                                                                                          |
| A message that starts `The connector couldn't mint an AWS outbound identity token`                                                                                                                                                                                                                                                                     | Check that AWS outbound identity federation is on for the connector's account, and apply the connector's Terraform again, as in [#step-2-deploy-the-connector](#step-2-deploy-the-connector "mention"). It lets the connector's role mint the token.                                         |
| A message that starts `The Google metadata server didn't mint an identity token for the connector's service account`                                                                                                                                                                                                                                   | Check that the Cloud Run service runs as the connector's service account.                                                                                                                                                                                                                    |
| `Tailscale didn't accept the connector's credential (<status>): <reason>. Check the client ID, then the OAuth client's secret, or the federated identity's issuer and subject. The client ID can't be changed in place: if it's wrong, remove this tailnet from P0 and add it again with the right one.`                                               | Check the client ID on the tailnet's page, and the OAuth client's secret or the federated identity's issuer and subject, as in [#step-3-create-the-tailscale-credential](#step-3-create-the-tailscale-credential "mention"). If the client ID is wrong, remove the tailnet and add it again. |
| A message that starts `Tailscale tailnet <tailnet> couldn't find` and says `Check that the tailnet still exists`                                                                                                                                                                                                                                       | Check the Tailnet ID in Tailscale's **Settings** > **General**. If it's wrong, remove the tailnet and add it again with the right one.                                                                                                                                                       |
| A message that starts `The connector's Tailscale credential is missing`, and lists scopes                                                                                                                                                                                                                                                              | Add each scope it lists to the credential, as in step 3.                                                                                                                                                                                                                                     |
| A message that starts `The connector's Tailscale credential carries`, and names `policy_file`, `all`, or both                                                                                                                                                                                                                                          | The credential could write the policy file. Create a credential with only the scopes in step 3, remove the tailnet, and add it again with the new client ID.                                                                                                                                 |
| `Tailscale didn't say which scopes the connector's federated credential carries, so P0 can't confirm it can't write the policy file. Check that the federated identity has only policy_file:read, users:read, devices:core:read, devices:posture_attributes, devices:routes:read, services:read, then validate again, or use an OAuth client instead.` | Check that the federated identity has only the scopes in step 3, and click **Next** again. If it keeps failing, create an OAuth client as in step 3, remove the tailnet, and add it again with the OAuth client's ID.                                                                        |
| `Tailnet <tailnet> has no untagged device of its own, so P0 can't check that it may set device attributes. Add one, then validate again.`                                                                                                                                                                                                              | Sign a device in to the tailnet as a person, not with tags. A device shared in from another tailnet doesn't count.                                                                                                                                                                           |
| A message that starts `Tailnet <tailnet> refused a custom device attribute`                                                                                                                                                                                                                                                                            | Check that the tailnet is on a Tailscale Premium or Enterprise plan, and that the credential has `devices:posture_attributes`. Any reason Tailscale gave follows the status.                                                                                                                 |
| `P0 invoked <where it runs>, but it didn't answer as the Tailscale connector. Check that it runs the connector's image, and check its logs, then retry.`                                                                                                                                                                                               | Apply the connector's Terraform again. If it already runs the connector, check its logs.                                                                                                                                                                                                     |
| `P0 couldn't reach the connector, <where it runs>. It may have been removed, or P0 may no longer be allowed to invoke it. Check that the connector's Terraform is still applied, then retry.`                                                                                                                                                          | Check that the connector runs where its page in P0 says, and apply its Terraform again.                                                                                                                                                                                                      |
| `Tailscale is temporarily unavailable. Try again in a few minutes.`                                                                                                                                                                                                                                                                                    | Tailscale is rate limiting the connector, isn't answering, or the connector can't reach it. Click **Next** again in a few minutes. If it keeps failing, check that the connector can reach the internet.                                                                                     |

### On requests

Requestors and approvers see these messages on a request. A failed revoke has a **Retry revoke** button on the request's page, and lists its messages after `P0 encountered one or more errors while revoking access`. A failed grant has no button. For the messages a requestor can fix themselves, see [Requesting Tailscale access](/integrations/resource-integrations/tailscale/requesting-access.md#when-p0-refuses-your-request).

| Message                                                                                                                                                                                                       | What to do                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A message that includes `refused a custom device attribute`                                                                                                                                                   | Check that the tailnet is on a Tailscale Premium or Enterprise plan, and that the credential has `devices:posture_attributes`. The requestor then requests again, or you click **Retry revoke**.                                                                                                                                                                                                           |
| `Tailscale tailnet <tailnet> refused the change (<status>): <reason>`                                                                                                                                         | Fix what Tailscale's reason names. The requestor then requests again, or you click **Retry revoke**.                                                                                                                                                                                                                                                                                                       |
| A message that starts `There is an unrecoverable error in your organization's Tailscale installation`                                                                                                         | The rest of the message says whether Tailscale refused the connector's credential, or refused it a call for a missing scope. Store the OAuth client's current secret as a new version, fix the federated identity's issuer and subject, or add every scope in [#step-3-create-the-tailscale-credential](#step-3-create-the-tailscale-credential "mention") to the credential. You don't need to reinstall. |
| `P0 couldn't reach the Tailscale connector. Ask your P0 admin to check its deployment.`                                                                                                                       | Check that the connector runs where its page in P0 says, and apply its Terraform again. Check that P0's AWS IAM management or Google Cloud IAM management is still installed for its account or project.                                                                                                                                                                                                   |
| `P0's Tailscale connector couldn't read the OAuth client's secret. Ask your P0 admin to check the Tailscale install.`                                                                                         | Check that the secret named on the tailnet's page exists and holds the client's secret, and that you applied the Terraform that lets the connector read it.                                                                                                                                                                                                                                                |
| `The Tailscale connector for tailnet <tailnet> isn't installed, so P0 can't grant access on it. Ask your P0 admin to install it. Access that P0 already granted on <tailnet> may need to be removed by hand.` | Install the connector again as in steps 1 and 2, with the ID and hosting it had. Then click **Retry revoke** on any failed revoke, or remove the posture's `custom:p0_` attribute from the device by hand.                                                                                                                                                                                                 |
| `Tailscale is temporarily unavailable. Try again in a few minutes.`                                                                                                                                           | Try again in a few minutes.                                                                                                                                                                                                                                                                                                                                                                                |

## Change the install later

* **Rotate an OAuth client's secret.** Rotate it in Tailscale, read it into your shell as in [#step-5-finish-the-tailnets-setup](#step-5-finish-the-tailnets-setup "mention"), and store it as a new version of the same secret:

  ```bash
  printf %s "$TAILSCALE_CLIENT_SECRET" | aws secretsmanager put-secret-value \
    --secret-id <secret-name> \
    --secret-string file:///dev/stdin \
    --region <region>
  ```

  Or, in Google Secret Manager:

  ```bash
  printf %s "$TAILSCALE_CLIENT_SECRET" | gcloud secrets versions add <secret-name> \
    --project <project-id> \
    --data-file=-
  ```

  Then run `unset TAILSCALE_CLIENT_SECRET`. The connector uses the new version on its next call, so nothing changes in P0 or your Terraform.
* **Change a tailnet's connector or credential.** Revoke the tailnet's grants, then remove the tailnet and add it again with the new values. P0 can't take access back on a tailnet that isn't installed. Check for active grants on the **Activity** tab of Access Management. See [Access Management](/access-management/access-management.md).
* **Move a connector.** Remove it, then add it again with the new hosting. Do this only when none of its tailnets has active grants. If the connector's identity changes, each tailnet's credential must trust the new one: change each federated identity's issuer and subject, or apply each OAuth client tailnet's Terraform that lets the connector read the secret again. A tailnet whose OAuth client secret is in the other cloud must be removed and added again.
* **Remove a connector.** Removing a connector or a tailnet revokes nothing. Revoke the grants on the tailnets that use the connector, remove those tailnets, then remove the connector. If you remove a connector that a tailnet still uses, P0 can't grant or revoke access on those tailnets until you add the connector again with the same ID and hosting. The posture attributes P0 already set stay on their devices until they expire.
* **Copy the Terraform again.** Open a connector or a tailnet you've set up, and expand **Show the connector's Terraform**, **Show the federated identity's Terraform**, **Show the Terraform that lets the connector read the secret**, or **Show the Terraform for the secret's account**.

## Limitations

* **A grant and a revoke take effect within seconds, not at once.** Within seconds of approval, the requestor's device can open the connections the posture gates. Within seconds of a revoke or expiry, it can't open new ones.
* **A revoke stops new connections only.** A Tailscale SSH session that's open when access ends stays open until the requestor closes it.
* **The integration needs a Tailscale Premium or Enterprise plan.** Other plans have no custom device posture attributes.
* **A grant covers one device.** P0 sets the posture on the one device the requestor picks, and refuses tagged devices and devices shared in from another tailnet.
* **A posture reaches only what your policy file gates on it.** P0 never writes the policy file, so to offer new access, add a posture and the grants that require it.
* **A posture can't be an item of an Access Bundle.** Request it on its own.
* **On AWS, one connector serves a few dozen tailnets with OAuth clients.** Each such tailnet adds a policy to the connector's role, and AWS limits the size of a role's policies. Past that, add another connector.
