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',
+ },
+ },
+ },
+ },
+ },
+ });
+
});