Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
author: 'rehanvdm',
authorAddress: 'rehan.vdm+github-tailscale-lambda-proxy@gmail.com',
cdkVersion: '2.225.0',
constructsVersion: '10.5.1',
defaultReleaseBranch: 'main',
jsiiVersion: '5.9.36',
name: 'tailscale-lambda-proxy',
Expand Down
76 changes: 76 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
- [Code Examples](#code-examples)
- [Additional Information](#additional-information)
- [AWS SigV4 Headers](#aws-sigv4-headers)
- [OAuth Client Keys (Recommended)](#oauth-client-keys-recommended)
- [Exit Nodes](#exit-nodes)

A CDK construct that creates an AWS Lambda Function acting as a transparent proxy to your Tailscale network.

Expand Down Expand Up @@ -47,6 +49,9 @@ Use the Proxy Lambda (recommended) if:
> [!TIP]
> Refer to the [tailscale-lambda-proxy-example](https://github.com/rehanvdm/tailscale-lambda-proxy-example) repository
> for a complete example.
>
> The [Tailscale Lambda Extension](https://github.com/rehanvdm/tailscale-lambda-extension) documents options in more
>

### Installation

Expand All @@ -56,7 +61,7 @@ npm install tailscale-lambda-proxy
```

The Proxy Lambda requires the following:
- `tsSecretApiKey`: The AWS Secrets Manager secret containing the Tailscale API Key as plain text.
- `tsSecretApiKey`: The AWS Secrets Manager secret containing the Tailscale API Key (auth key or OAuth client key).
- `tsHostname`: The "Machine" name as shown in the Tailscale admin console, which identifies the Lambda function(s).

```typescript
Expand Down Expand Up @@ -90,6 +95,11 @@ export class MyStack extends cdk.Stack {
// functionName: name("tailscale-proxy-warmer"),
// concurrentInvocations: 2,
// }
// tsAdvertiseTags: "tag:lambda", // Required for OAuth client keys (non-expiring); sets --advertise-tags
// tsExitNode: "100.x.y.z", // Route all internet-bound traffic through this Tailscale exit node
// tsExitNodeRequired: true, // Abort if the exit node is unreachable (fail-closed); default: false
// tsExitNodePingTimeout: 2000, // Per-ping timeout (ms) when checking exit node reachability; default: 2000
// tsExitNodePingRetries: 10, // Number of ping attempts before giving up on the exit node; default: 10
});

const caller = new NodejsFunction(this, "tailscale-caller", {
Expand Down Expand Up @@ -235,6 +245,31 @@ export const handler = async (event: any) => {
};
```

## Extension Options

The TS Lambda Extension accepts various options that can be configured through environment variables. These include:

| TS Option | Environment Variable | Description |
|-----------|---------------------|-------------|
| `tsAdvertiseTags` | `TS_ADVERTISE_TAGS` | Enables OAuth client keys when set, allowing for non-expiring keys and more secure authentication. |
| `tsExitNode` | `TS_EXIT_NODE` | Routes internet-bound traffic through a specified Tailscale exit node. |
| `tsExitNodeRequired` | `TS_EXIT_NODE_REQUIRED` | When set to `true`, the extension will fail to initialize if the specified exit node is not reachable, ensuring critical routing requirements are met. |
| `tsExitNodePingTimeout` | `TS_EXIT_NODE_PING_TIMEOUT` | Configures the timeout (ms) for exit node reachability checks. |
| `tsExitNodePingRetries` | `TS_EXIT_NODE_PING_RETRIES` | Configures the number of retry attempts for exit node reachability checks. |

These can be set directly on the construct props:
```ts
const tailscaleProxy = new TailscaleLambdaProxy(this, "tailscale-proxy", {
tsSecretApiKey: secretsmanager.Secret.fromSecretNameV2(this, "tailscale-api-key", "tailscale-api-key"),
tsHostname: "lambda-test",
tsAdvertiseTags: 'tag:aws',
tsExitNode: "100.1.2.3",
tsExitNodeRequired: true,
tsExitNodePingRetries: 10,
tsExitNodePingTimeout: 2000,
});
```

## Additional Information

Refer to the [Tailscale Lambda Extension](https://github.com/rehanvdm/tailscale-lambda-extension) documentation for:
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 37 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,37 @@ export interface TailscaleLambdaProxyProps {
*/
readonly tsHostname: string;

/**
* Passed as `--advertise-tags` to `tailscale up`. Required when using OAuth client keys (no expiry).
* Example: `tag:lambda`
*/
readonly tsAdvertiseTags?: string;

/**
* Passed as `--exit-node` to `tailscale up`. Routes all internet-bound Lambda traffic through the
* specified exit node (Tailscale IP or hostname). Example: `100.x.y.z`
*/
readonly tsExitNode?: string;

/**
* When `true`, the extension aborts (fail-closed) if the exit node is not reachable within the
* timeout. When `false` / unset, it logs a warning and continues (fail-open).
* @default false
*/
readonly tsExitNodeRequired?: boolean;

/**
* Per-attempt timeout in milliseconds for the exit node reachability ping.
* @default 2000
*/
readonly tsExitNodePingTimeout?: number;

/**
* Number of ping attempts before giving up on the exit node reachability check.
* @default 10
*/
readonly tsExitNodePingRetries?: number;

readonly options?: TailscaleLambdaProxyPropsOptions;

readonly debug?: boolean;
Expand Down Expand Up @@ -69,8 +100,13 @@ export class TailscaleLambdaProxy extends Construct {
environment: {
TS_SECRET_API_KEY: props.tsSecretApiKey.secretArn,
TS_HOSTNAME: props.tsHostname,
...(props?.debug) ? { DEBUG: 'true' } : { },
...(props.debug) ? { DEBUG: 'true' } : {},
...(props.options?.lambda?.nodeTlsRejectUnauthorized === false) ? { NODE_TLS_REJECT_UNAUTHORIZED: '0' } : {},
...(props.tsAdvertiseTags) ? { TS_ADVERTISE_TAGS: props.tsAdvertiseTags } : {},
...(props.tsExitNode) ? { TS_EXIT_NODE: props.tsExitNode } : {},
...(props.tsExitNodeRequired === true) ? { TS_EXIT_NODE_REQUIRED: 'true' } : {},
...(props.tsExitNodePingTimeout !== undefined) ? { TS_EXIT_NODE_PING_TIMEOUT: props.tsExitNodePingTimeout.toString() } : {},
...(props.tsExitNodePingRetries !== undefined) ? { TS_EXIT_NODE_PING_RETRIES: props.tsExitNodePingRetries.toString() } : {},
},
timeout: cdk.Duration.minutes(15),
memorySize: 256,
Expand Down