Skip to content

terraform/aws: add aws-app-connector-s3-privatelink - #51

Draft
maxiscoding28 wants to merge 6 commits into
tailscale-dev:mainfrom
maxiscoding28:add-aws-app-connector-s3-privatelink
Draft

terraform/aws: add aws-app-connector-s3-privatelink#51
maxiscoding28 wants to merge 6 commits into
tailscale-dev:mainfrom
maxiscoding28:add-aws-app-connector-s3-privatelink

Conversation

@maxiscoding28

@maxiscoding28 maxiscoding28 commented Jul 29, 2026

Copy link
Copy Markdown

Proposed in #52.

What it does

Adds terraform/aws/aws-app-connector-s3-privatelink/. Four files, 436 lines.

The example routes traffic for one S3 bucket over a private path. Every other bucket, and all other traffic, keeps egressing publicly.

Why it is worth an example

Pointing an app connector at a public S3 endpoint does not work well. S3 regional endpoints resolve to a large pool of addresses that rotates. The connector keeps discovering new addresses, so the route set grows and never settles.

The fix is to give the bucket a stable private address first, then advertise that. Three pieces have to line up:

  1. An S3 interface VPC endpoint with private_dns_only_for_inbound_resolver_endpoint = false. Resolving the bucket domain inside the VPC now returns the endpoint ENI.
  2. A split DNS entry on the exact bucket FQDN. Split DNS does not match siblings, so other buckets stay public. This is what scopes the change to one bucket.
  3. The connector advertises the VPC resolver and the endpoint ENI. The resolver is the only one that knows the private DNS mapping, and a client outside the VPC cannot reach it directly.

Checks

All three jobs from terraform-examples.yml, run against a clean clone:

Check Result
tflint --chdir=terraform --recursive pass
check-terraform-fmt.sh pass
check-variables-tailscale-install-scripts.sh pass
terraform validate pass

Applied end to end in a sandbox account (us-west-2) and verified:

  • 32 resources created, no changes to existing infrastructure
  • the bucket resolves to the endpoint ENI from a tailnet client, and to rotating public S3 addresses from off-tailnet
  • the object returns 200 over the private path and 403 from the public internet
  • both routes advertised and enabled with no autoApprovers entry, confirming policy-file routes are implicitly approved
  • the split DNS entry was added alongside existing entries rather than replacing the tailnet DNS config

Style guide

  • One locals block at the top of main.tf with every customizable value
  • example-${basename(path.cwd)} prefix, Name tag on every resource that takes tags
  • Local names match the mock example: vpc_cidr_block, instance_subnet_id, instance_security_group_ids
  • Uses internal-modules/aws-vpc and internal-modules/aws-ec2-instance, both removable
  • terraform fmt clean, # comments, AWS provider >= 6.0, < 7.0, Tailscale >= 0.24

What the user provides

The example takes no Terraform inputs. It needs AWS and Tailscale credentials in the environment, and two edits to your policy file.

Before applying, tag:example-infra and tag:example-appconnector need owners, or key creation fails. Every example in the repo needs this.

After applying, the app connector definition has to be added by hand. Terraform creates the split DNS entry and the device key, but not this. The provider has no app connector resource, and the definition lives in nodeAttrs in the policy file. The only way to write that from Terraform is tailscale_acl, which replaces the whole policy file, so no example in this repo uses it.

The README shows the stanza, and the s3_domain and s3_advertised_routes outputs give the two values that need filling in.

maxiscoding28 and others added 6 commits July 28, 2026 16:11
Route one specific S3 bucket privately through a Tailscale app connector
while all other buckets and web traffic continue to egress publicly.

An app connector pointed at a public S3 endpoint advertises whatever that
endpoint resolves to. S3 regional endpoints are large shared address pools
that rotate, so the advertised route set grows without bound. This example
gives the bucket a stable private address with an S3 interface VPC endpoint
first, then advertises that one address, so the route set is a fixed pair.

Three pieces have to line up:

  1. The interface endpoint, with private DNS and
     private_dns_only_for_inbound_resolver_endpoint = false, gives the
     bucket a stable ENI address inside the VPC.
  2. A split DNS entry on the exact bucket FQDN sends only that one name to
     the VPC resolver. Sibling buckets do not match and stay public.
  3. The connector advertises the VPC resolver and the endpoint ENI, and
     SNATs both so they arrive as ordinary in-VPC traffic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Match the mock example in CONTRIBUTING.md more closely. Cut comments from
51 lines to 26, in line with the other AWS examples. Move the subnet CIDR
into locals and use the same local names as the mock (vpc_cidr_block,
instance_subnet_id, instance_security_group_ids). Add the missing Name tag
on the S3 object and rename resources to "main".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Tailscale provider has no app connector resource, so that stanza has to
be added to the policy file by hand. Add a next_step_app_connector_policy
output that prints it with the bucket domain and advertised routes filled
in, ready to paste. Say plainly in the README why this step is manual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The connector was passing both --advertise-connector and --advertise-routes.
The second flag is not needed. App connector routes belong in the "routes"
field of the app connector definition, where they are implicitly approved
and need no autoApprovers entry.

Move the routes into the stanza the output prints and drop the autoApprovers
block from the README, which is no longer required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The policy example mixed a prerequisite with a post-apply step. The tag
owners have to exist before apply or key creation fails, while the app
connector definition needs values the apply produces.

Split the section into "before you apply" and "after you apply", and say in
the deploy steps that the tag owners come first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No other example prints instructions as an output. The README already shows
the stanza, and s3_domain and s3_advertised_routes carry the two values that
need filling in, so point at those instead.

Also note that the VPC resolver address is derived from vpc_cidr_block, which
matters when swapping in your own VPC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant