Skip to content

Commit

Permalink
tailscale: add support for S3 logstreaming
Browse files Browse the repository at this point in the history
We recently added support for S3 logstreaming endpoints to our API. This
involved adding several new fields to the LogstreamConfiguration resource
(available when the new "s3" destinationType is used), plus a new
AWSExternalID resource needed when AWS role-based authentication is used
for an S3 logstreaming endpoint.

This commit updates the Terraform provider to reflect these changes. We add
support for the new fields to the tailscale_logstream_configuration
resource, and create a new tailscale_aws_external_id resource.

Unfortunately, we have to add some special handling for the "user" field of
tailscale_logstream_configuration. Previously, we specified a default dummy
value of "user" for the "user" field. Now, the PUT LogstreamConfiguration
endpoint does not allow any value to be specified for a s3 destinationType.
We could loosen that restriction at the API layer, but I would prefer not
to do that. However, we don't want to break any existing users who are
relying on this default and have it in their Terraform state. So the
approach I've chosen is to add special handling for the "user" field:
- When creating or updating a configuration, if destinationType == "s3" and
  user == "user", we assume the "user" value came from the default and we
  do not actually send it to the API.
- When reading or importing a configuration, if destinationType == "s3" and
  there is no user field (which should always be true), we set
  user = "user" in the Terraform state.

Fixes #458
Fixes tailscale/corp#24533

Signed-off-by: Zach Hauser <[email protected]>
  • Loading branch information
zehauser committed Feb 7, 2025
1 parent bc18edf commit 9ba5316
Show file tree
Hide file tree
Showing 11 changed files with 513 additions and 14 deletions.
72 changes: 72 additions & 0 deletions docs/resources/aws_external_id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "tailscale_aws_external_id Resource - terraform-provider-tailscale"
subcategory: ""
description: |-
The aws_external_id resource allows you to mint an AWS External ID that Tailscale can use to assume an AWS IAM role that you create for the purposes of allowing Tailscale to stream logs to your S3 bucket. See the logstream_configuration resource for more details.
---

# tailscale_aws_external_id (Resource)

The aws_external_id resource allows you to mint an AWS External ID that Tailscale can use to assume an AWS IAM role that you create for the purposes of allowing Tailscale to stream logs to your S3 bucket. See the logstream_configuration resource for more details.

## Example Usage

```terraform
resource "tailscale_aws_external_id" "prod" {}
resource "tailscale_logstream_configuration" "configuration_logs" {
log_type = "configuration"
destination_type = "s3"
s3_bucket = aws_s3_bucket.tailscale_logs.id
s3_region = "us-west-2"
s3_authentication_type = "rolearn"
s3_role_arn = aws_iam_role.logs_writer.arn
s3_external_id = tailscale_aws_external_id.prod.external_id
}
resource "aws_iam_role" "logs_writer" {
name = "logs-writer"
assume_role_policy = data.aws_iam_policy_document.tailscale_assume_role.json
}
resource "aws_iam_role_policy" "logs_writer" {
role = aws_iam_role.logs_writer.id
policy = data.aws_iam_policy_document.logs_writer.json
}
data "aws_iam_policy_document" "tailscale_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [tailscale_aws_external_id.prod.tailscale_aws_account_id]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [tailscale_aws_external_id.prod.external_id]
}
}
}
data "aws_iam_policy_document" "logs_writer" {
statement {
effect = "Allow"
actions = ["s3:*"]
resources = [
"arn:aws:s3:::example-bucket",
"arn:aws:s3:::example-bucket/*"
]
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Read-Only

- `external_id` (String) The External ID that Tailscale will supply when assuming your role. You must reference this in your IAM role's trust policy. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html for more information on external IDs.
- `id` (String) The ID of this resource.
- `tailscale_aws_account_id` (String) The AWS account from which Tailscale will assume your role. You must reference this in your IAM role's trust policy. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html for more information on external IDs.
39 changes: 37 additions & 2 deletions docs/resources/logstream_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,39 @@ The logstream_configuration resource allows you to configure streaming configura
## Example Usage

```terraform
# Example configuration for a non-S3 logstreaming endpoint
resource "tailscale_logstream_configuration" "sample_logstream_configuration" {
log_type = "configuration"
destination_type = "panther"
url = "https://example.com"
token = "some-token"
}
# Example configuration for an AWS S3 logstreaming endpoint
resource "tailscale_logstream_configuration" "sample_logstream_configuration_s3" {
log_type = "configuration"
destination_type = "s3"
s3_bucket = aws_s3_bucket.tailscale_logs.id
s3_region = "us-west-2"
s3_authentication_type = "rolearn"
s3_role_arn = aws_iam_role.tailscale_logs_writer.arn
s3_external_id = tailscale_aws_external_id.prod.external_id
}
# Example configuration for an S3-compatible logstreaming endpoint
resource "tailscale_logstream_configuration" "sample_logstream_configuration_s3_compatible" {
log_type = "configuration"
destination_type = "s3"
url = "https://s3.example.com"
s3_bucket = "example-bucket"
s3_region = "us-west-2"
s3_authentication_type = "accesskey"
s3_access_key_id = "some-access-key"
s3_secret_access_key = "some-secret-key"
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -28,11 +55,19 @@ resource "tailscale_logstream_configuration" "sample_logstream_configuration" {

- `destination_type` (String) The type of system to which logs are being streamed.
- `log_type` (String) The type of log that is streamed to this endpoint.
- `token` (String, Sensitive) The token/password with which log streams to this endpoint should be authenticated.
- `url` (String) The URL to which log streams are being posted.

### Optional

- `s3_access_key_id` (String) The S3 access key ID. Required if destination_type is s3 and s3_authentication_type is 'accesskey'.
- `s3_authentication_type` (String) What type of authentication to use for S3. Required if destination_type is 's3'. Tailscale recommends using 'rolearn'.
- `s3_bucket` (String) The S3 bucket name. Required if destination_type is 's3'.
- `s3_external_id` (String) The AWS External ID that Tailscale supplies when authenticating using role-based authentication. Required if destination_type is 's3' and s3_authentication_type is 'rolearn'. This can be obtained via the tailscale_aws_external_id resource.
- `s3_key_prefix` (String) An optional S3 key prefix to prepend to the auto-generated S3 key name.
- `s3_region` (String) The region in which the S3 bucket is located. Required if destination_type is 's3'.
- `s3_role_arn` (String) ARN of the AWS IAM role that Tailscale should assume when using role-based authentication. Required if destination_type is 's3' and s3_authentication_type is 'rolearn'.
- `s3_secret_access_key` (String, Sensitive) The S3 secret access key. Required if destination_type is 's3' and s3_authentication_type is 'accesskey'.
- `token` (String, Sensitive) The token/password with which log streams to this endpoint should be authenticated, required unless destination_type is 's3'.
- `url` (String) The URL to which log streams are being posted. If destination_type is 's3' and you want to use the official Amazon S3 endpoint, leave this empty.
- `user` (String) The username with which log streams to this endpoint are authenticated. Only required if destination_type is 'elastic', defaults to 'user' if not set.

### Read-Only
Expand Down
47 changes: 47 additions & 0 deletions examples/resources/tailscale_aws_external_id/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
resource "tailscale_aws_external_id" "prod" {}

resource "tailscale_logstream_configuration" "configuration_logs" {
log_type = "configuration"
destination_type = "s3"
s3_bucket = aws_s3_bucket.tailscale_logs.id
s3_region = "us-west-2"
s3_authentication_type = "rolearn"
s3_role_arn = aws_iam_role.logs_writer.arn
s3_external_id = tailscale_aws_external_id.prod.external_id
}

resource "aws_iam_role" "logs_writer" {
name = "logs-writer"
assume_role_policy = data.aws_iam_policy_document.tailscale_assume_role.json
}

resource "aws_iam_role_policy" "logs_writer" {
role = aws_iam_role.logs_writer.id
policy = data.aws_iam_policy_document.logs_writer.json
}

data "aws_iam_policy_document" "tailscale_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [tailscale_aws_external_id.prod.tailscale_aws_account_id]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [tailscale_aws_external_id.prod.external_id]
}
}
}

data "aws_iam_policy_document" "logs_writer" {
statement {
effect = "Allow"
actions = ["s3:*"]
resources = [
"arn:aws:s3:::example-bucket",
"arn:aws:s3:::example-bucket/*"
]
}
}
27 changes: 27 additions & 0 deletions examples/resources/tailscale_logstream_configuration/resource.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
# Example configuration for a non-S3 logstreaming endpoint

resource "tailscale_logstream_configuration" "sample_logstream_configuration" {
log_type = "configuration"
destination_type = "panther"
url = "https://example.com"
token = "some-token"
}

# Example configuration for an AWS S3 logstreaming endpoint

resource "tailscale_logstream_configuration" "sample_logstream_configuration_s3" {
log_type = "configuration"
destination_type = "s3"
s3_bucket = aws_s3_bucket.tailscale_logs.id
s3_region = "us-west-2"
s3_authentication_type = "rolearn"
s3_role_arn = aws_iam_role.tailscale_logs_writer.arn
s3_external_id = tailscale_aws_external_id.prod.external_id
}

# Example configuration for an S3-compatible logstreaming endpoint

resource "tailscale_logstream_configuration" "sample_logstream_configuration_s3_compatible" {
log_type = "configuration"
destination_type = "s3"
url = "https://s3.example.com"
s3_bucket = "example-bucket"
s3_region = "us-west-2"
s3_authentication_type = "accesskey"
s3_access_key_id = "some-access-key"
s3_secret_access_key = "some-secret-key"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0
github.com/stretchr/testify v1.10.0
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20241028210109-bd4d815eb293
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20250129222324-74c8fc3cb4d7
golang.org/x/tools v0.29.0
tailscale.com v1.80.0
)
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20241028210109-bd4d815eb293 h1:qguJy85YoqTEdGznTqbOSh+GZgTvBpOCriN9/HhayK0=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20241028210109-bd4d815eb293/go.mod h1:i/MSgQ71kdyh1Wdp50XxrIgtsyO4uZ2SZSPd83lGKHM=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20250122081626-c84613baf18d h1:I3Ka+YodSpslWRaHHOtKQiHFcfuTr8o23619oP1P11I=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20250122081626-c84613baf18d/go.mod h1:i/MSgQ71kdyh1Wdp50XxrIgtsyO4uZ2SZSPd83lGKHM=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20250129222324-74c8fc3cb4d7 h1:mNv0N8L5geeR9d4FKecN1WoebLmWx52i30GRh4qKabQ=
github.com/tailscale/tailscale-client-go/v2 v2.0.0-20250129222324-74c8fc3cb4d7/go.mod h1:i/MSgQ71kdyh1Wdp50XxrIgtsyO4uZ2SZSPd83lGKHM=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down
1 change: 1 addition & 0 deletions tailscale/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func Provider(options ...ProviderOption) *schema.Provider {
"tailscale_contacts": resourceContacts(),
"tailscale_posture_integration": resourcePostureIntegration(),
"tailscale_logstream_configuration": resourceLogstreamConfiguration(),
"tailscale_aws_external_id": resourceAWSExternalID(),
"tailscale_tailnet_settings": resourceTailnetSettings(),
},
DataSourcesMap: map[string]*schema.Resource{
Expand Down
61 changes: 61 additions & 0 deletions tailscale/resource_aws_external_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) David Bond, Tailscale Inc, & Contributors
// SPDX-License-Identifier: MIT

package tailscale

import (
"context"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

tsclient "github.com/tailscale/tailscale-client-go/v2"
)

func resourceAWSExternalID() *schema.Resource {
return &schema.Resource{
Description: "The aws_external_id resource allows you to mint an AWS External ID that Tailscale can use to assume an AWS IAM role that you create for the purposes of allowing Tailscale to stream logs to your S3 bucket. See the logstream_configuration resource for more details.",
CreateContext: resourceAWSExternalIDCreate,

// No GET or DELETE endpoints in the API. This is a create-only resource.
ReadContext: schema.NoopContext,
DeleteContext: schema.NoopContext,

Schema: map[string]*schema.Schema{
"external_id": {
Type: schema.TypeString,
Computed: true,
Description: "The External ID that Tailscale will supply when assuming your role. You must reference this in your IAM role's trust policy. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html for more information on external IDs.",
},
"tailscale_aws_account_id": {
Type: schema.TypeString,
Computed: true,
Description: "The AWS account from which Tailscale will assume your role. You must reference this in your IAM role's trust policy. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html for more information on external IDs.",
},
},
}
}

func resourceAWSExternalIDCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*tsclient.Client)

// We pass "reusable: false" on purpose. Otherwise, two tailscale_aws_external_id resources
// could end up with the same resource ID (because we use the actual external ID).
//
// Also, "reusable: true" is an optimization intended for the admin console UI's usage
// pattern, and it's not really necessary for Terraform use cases.
aid, err := client.Logging().CreateOrGetAwsExternalId(ctx, false)
if err != nil {
return diagnosticsError(err, "Failed to create AWS External ID")
}

d.SetId(aid.ExternalID)
if err = d.Set("external_id", aid.ExternalID); err != nil {
return diagnosticsError(err, "Failed to set externalId")
}
if err = d.Set("tailscale_aws_account_id", aid.TailscaleAWSAccountID); err != nil {
return diagnosticsError(err, "Failed to set AWSAccountID")
}

return nil
}
32 changes: 32 additions & 0 deletions tailscale/resource_aws_external_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) David Bond, Tailscale Inc, & Contributors
// SPDX-License-Identifier: MIT

package tailscale

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

const testAWSExternalID = `
resource "tailscale_aws_external_id" "test" {}
`

func TestAccTailscaleAWSExternalID(t *testing.T) {
const resourceName = "tailscale_aws_external_id.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAWSExternalID,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "external_id"),
resource.TestCheckResourceAttrSet(resourceName, "tailscale_aws_account_id"),
),
},
},
})
}
Loading

0 comments on commit 9ba5316

Please sign in to comment.