diff --git a/API.md b/API.md index 5bb861d..f771e85 100644 --- a/API.md +++ b/API.md @@ -190,6 +190,7 @@ const tailscaleBastionProps: TailscaleBastionProps = { ... } | tailscaleCredentials | TailscaleCredentials | Credential settings for the tailscale auth key. | | vpc | aws-cdk-lib.aws_ec2.Vpc | VPC to launch the instance in. | | additionalInit | aws-cdk-lib.aws_ec2.InitElement[] | Additional cloudformation init actions to perform during startup. | +| advertiseRoute | string | Advertise a custom route instead of using the VPC CIDR, used for Tailscale 4via6 support. | | availabilityZone | string | In which AZ to place the instance within the VPC. | | incomingRoutes | string[] | List of incoming routes from Tailscale network. | | instanceName | string | The name of the instance. | @@ -238,6 +239,18 @@ Additional cloudformation init actions to perform during startup. --- +##### `advertiseRoute`Optional + +```typescript +public readonly advertiseRoute: string; +``` + +- *Type:* string + +Advertise a custom route instead of using the VPC CIDR, used for Tailscale 4via6 support. + +--- + ##### `availabilityZone`Optional ```typescript diff --git a/README.md b/README.md index 20f0a5f..17f8a4b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The Tailscale Auth key should be passed in via secrets manager and NOT hardcoded import { TailscaleBastion } from 'cdk-tailscale-bastion'; // Secrets Manager -const secret = Secret.fromSecretNameV2(stack, 'ApiSecrets', 'tailscale').secretValueFromJson('AUTH_KEY'); +const secret = Secret.fromSecretNameV2(stack, 'ApiSecrets', 'tailscale'); const bastion = new TailscaleBastion(stack, 'Sample-Bastion', { vpc, @@ -51,14 +51,27 @@ You'll also need to setup the nameserver. The bastion construct conveniently out Given your configuration is correct, a direct connection to your internal resources should now be possible. + +## 4via6 Support + +If you wish to use [4via6 subnet routers](https://tailscale.com/kb/1201/4via6-subnets/), you can pass the IPv6 address via the `advertiseRoute` property: + +```ts +new TailscaleBastion(stack, 'Cdk-Sample-Lib', { + vpc, + tailscaleCredentials: ..., + advertiseRoute: 'fd7a:115c:a1e0:b1a:0:7:a01:100/120', +}); +``` + ## Incoming routes If you have other subnet routers configured in Tailscale, you can use the `incomingRoutes` property to configure VPC route table entries for all private subnets. -``` +```ts new TailscaleBastion(stack, 'Sample-Bastion', { vpc, - tailscaleCredentials: ... + tailscaleCredentials: ..., incomingRoutes: [ '192.168.1.0/24', ], diff --git a/src/index.ts b/src/index.ts index 9d43a27..119fbea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { CfnOutput, Fn, Stack, Token } from 'aws-cdk-lib'; import { BastionHostLinux, CloudFormationInit, InitCommand, ISecurityGroup, Peer, Port, SubnetSelection, Vpc, InstanceType, SubnetType, InitElement, CfnRoute } from 'aws-cdk-lib/aws-ec2'; import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; import { Construct } from 'constructs'; @@ -78,6 +78,10 @@ export interface TailscaleBastionProps { * @default none */ readonly incomingRoutes?: string[]; + /** + * Advertise a custom route instead of using the VPC CIDR, used for Tailscale 4via6 support. + */ + readonly advertiseRoute?: string; } export class TailscaleBastion extends Construct { @@ -95,6 +99,7 @@ export class TailscaleBastion extends Construct { instanceType, additionalInit, incomingRoutes, + advertiseRoute, } = props; const authKeyCommand = this.computeTsKeyCli(tailscaleCredentials); @@ -115,14 +120,14 @@ export class TailscaleBastion extends Construct { InitCommand.shellCommand('yum -y install jq'), InitCommand.shellCommand('systemctl enable --now tailscaled'), InitCommand.shellCommand(`echo TS_AUTHKEY=${authKeyCommand} >> /etc/environment`), - InitCommand.shellCommand(`source /etc/environment && tailscale up --authkey $TS_AUTHKEY --advertise-routes=${props.vpc.vpcCidrBlock} --accept-routes --accept-dns=false`), + InitCommand.shellCommand(`source /etc/environment && tailscale up --authkey $TS_AUTHKEY --advertise-routes=${advertiseRoute ?? vpc.vpcCidrBlock} --accept-routes --accept-dns=false`), ...(additionalInit ?? []), ), initOptions: {}, }); - if (props.tailscaleCredentials.secretsManager) { - props.tailscaleCredentials.secretsManager.secret.grantRead(bastion); + if (tailscaleCredentials.secretsManager) { + tailscaleCredentials.secretsManager.secret.grantRead(bastion); } bastion.connections.allowFromAnyIpv4(Port.udp(41641)); @@ -133,7 +138,11 @@ export class TailscaleBastion extends Construct { const dnsServer = `${splitIp[0]}.${splitIp[1]}.${splitIp[2]}.2`; new CfnOutput(this, 'Vpc-Dns-Nameserver', { value: dnsServer }); - new CfnOutput(this, 'Vpc-Dns-Domain', { value: 'compute.internal' }); + + const stack = Stack.of(this); + const domain = Token.isUnresolved(stack.region) ? 'compute.internal' : `${stack.region}.compute.internal`; + + new CfnOutput(this, 'Vpc-Dns-Domain', { value: domain }); for (const incomingRoute of incomingRoutes ?? []) { for (const subnet of vpc.privateSubnets) { diff --git a/test/construct.test.ts b/test/construct.test.ts index d7347a8..a05bd1b 100644 --- a/test/construct.test.ts +++ b/test/construct.test.ts @@ -5,8 +5,8 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; import { TailscaleBastion } from '../src'; const mockApp = new App(); - -const stack = new Stack(mockApp, 'MyStack'); +const env = { region: 'ap-southeast-2' }; +const stack = new Stack(mockApp, 'MyStack', { env }); const vpc = new Vpc(stack, 'MyVpc'); @@ -72,19 +72,11 @@ test('Bastion host should be created', () => { 'Fn::Join': [ '', [ - 'echo TS_AUTHKEY=$(aws secretsmanager get-secret-value --region ', - { - Ref: 'AWS::Region', - }, - ' --secret-id arn:', + 'echo TS_AUTHKEY=$(aws secretsmanager get-secret-value --region ap-southeast-2 --secret-id arn:', { Ref: 'AWS::Partition', }, - ':secretsmanager:', - { - Ref: 'AWS::Region', - }, - ':', + ':secretsmanager:ap-southeast-2:', { Ref: 'AWS::AccountId', }, @@ -115,6 +107,10 @@ test('Bastion host should be created', () => { }, }, }); + + template.hasOutput('*', { + Value: 'ap-southeast-2.compute.internal', + }); }); diff --git a/test/routes.test.ts b/test/routes.test.ts index f3ea05b..877f2d9 100644 --- a/test/routes.test.ts +++ b/test/routes.test.ts @@ -23,6 +23,7 @@ const bastion = new TailscaleBastion(stack, 'Test-Bastion', { incomingRoutes: [ '192.168.1.0/24', ], + advertiseRoute: 'fd7a:115c:a1e0:b1a:0:7:a01:100/120', }); secret.grantRead(bastion.bastion); @@ -43,6 +44,20 @@ test('Bastion host should have routing set up', () => { }, }); + template.hasResource('AWS::EC2::Instance', { + Metadata: { + 'AWS::CloudFormation::Init': { + config: { + commands: { + '008': { + command: 'source /etc/environment && tailscale up --authkey $TS_AUTHKEY --advertise-routes=fd7a:115c:a1e0:b1a:0:7:a01:100/120 --accept-routes --accept-dns=false', + }, + }, + }, + }, + }, + }); + });