Skip to content
Open
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 .github/workflows/gitleaks-self-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ jobs:
uses: ./.github/workflows/gitleaks.yml
with:
fail_on_finding: false
config_path: .gitleaks.toml
88 changes: 88 additions & 0 deletions .github/workflows/netbird-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Netbird Infrastructure

# CDK app (C#) for the self-hosted Netbird VPN (control plane + routing peer) in the
# autoguru-shared account. Scoped to the netbird/ subtree so unrelated devsecops changes
# (workflows, docs) never touch this infrastructure.
#
# - Pull requests that touch netbird/** run `cdk diff` (read-only, no changes).
# - Deploys are MANUAL (workflow_dispatch) so infra is never applied implicitly by a
# merge to main; an operator triggers the deploy and reviews the diff in the run log.
#
# Requires the repository secret AWS_DEPLOY_ROLE_ARN to be the ARN of an IAM role in
# the shared account (791686214595) that this repo can assume via GitHub OIDC and that
# can drive the CDK deploy roles (the shared GitHubActionsRole already trusts
# repo:autoguru-au/* via OIDC).

on:
pull_request:
paths:
- 'netbird/**'
- '.github/workflows/netbird-deploy.yml'
workflow_dispatch:
inputs:
action:
description: 'CDK action to run'
required: true
default: 'diff'
type: choice
options:
- diff
- deploy

permissions:
id-token: write # OIDC auth to AWS -- no stored access keys
contents: read

defaults:
run:
working-directory: netbird/cdk

jobs:
cdk:
name: CDK ${{ github.event_name == 'pull_request' && 'diff' || inputs.action }}
runs-on: ubuntu-latest
env:
# Mapped to an env var so steps can detect whether the deploy role is configured.
# Until an admin sets this repo secret, the AWS steps are skipped (the compile check
# still runs) so the PR check is not red purely because the secret is not wired yet.
AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Install CDK CLI
# Pinned to the same minor as Amazon.CDK.Lib in the csproj so a floating CLI
# release cannot surprise the pipeline with a lib/CLI version skew.
run: npm install -g aws-cdk@2.140.0

- name: Build (compile check)
run: dotnet build -c Release

- name: Note missing deploy role
if: env.AWS_DEPLOY_ROLE_ARN == ''
run: echo "::warning::AWS_DEPLOY_ROLE_ARN secret is not configured; skipping the AWS steps (cdk diff/deploy). The compile check above still ran. Set the repo secret to enable diff/deploy."

- uses: aws-actions/configure-aws-credentials@v4
if: env.AWS_DEPLOY_ROLE_ARN != ''
with:
role-to-assume: ${{ env.AWS_DEPLOY_ROLE_ARN }}
aws-region: ap-southeast-2

- name: CDK Diff (pull requests)
if: github.event_name == 'pull_request' && env.AWS_DEPLOY_ROLE_ARN != ''
run: cdk diff

- name: CDK Diff (manual)
if: github.event_name == 'workflow_dispatch' && inputs.action == 'diff' && env.AWS_DEPLOY_ROLE_ARN != ''
run: cdk diff

- name: CDK Deploy (manual)
if: github.event_name == 'workflow_dispatch' && inputs.action == 'deploy' && env.AWS_DEPLOY_ROLE_ARN != ''
run: cdk deploy NetbirdControlPlaneStack NetbirdRoutingPeerStack --require-approval never
20 changes: 20 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
title = "AutoGuru devsecops gitleaks config"

# Keep all of gitleaks' built-in rules; this file only adds a narrow allowlist.
[extend]
useDefault = true

[allowlist]
description = """
Public Microsoft Entra ID OIDC identifiers used by the Netbird control-plane setup
(netbird/scripts/control-plane-user-data.sh). The application (client) ID, tenant ID
and audience are NOT secrets: they are public identifiers that ship in client config.
The only secret, the client secret, is pulled from AWS Secrets Manager at runtime
(NETBIRD_AUTH_CLIENT_SECRET=${ENTRA_SECRET}) and is never committed. These two exact
GUIDs are allowlisted so the generic-api-key rule stops flagging them; a genuinely
sensitive value would still be caught (this is value-scoped, not path-scoped).
"""
regexes = [
'''5853144b-3c6f-4e39-a5b0-df1c3efcdcb1''',
'''4542d3b9-a2ab-47a6-bc7a-1c25894c1adf''',
]
8 changes: 8 additions & 0 deletions netbird/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Force LF under netbird/: this subtree deploys to Linux (EC2 user-data, docker, CI).
# CRLF in a bash user-data script breaks the shebang on the instance. The .cs files embed
# those scripts as raw content, so they must be LF too.
* text=auto eol=lf
*.sh text eol=lf
*.cs text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
11 changes: 11 additions & 0 deletions netbird/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# .NET build output
bin/
obj/

# CDK
cdk.out/
cdk.context.json

# Environment / secrets
.env
*.env
69 changes: 69 additions & 0 deletions netbird/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Netbird (self-hosted VPN)

Infrastructure as Code for AutoGuru's self-hosted [Netbird](https://netbird.io) deployment,
per [ADR-002: Netbird VPN Replacement](https://autoguru.atlassian.net/wiki/spaces/CS/pages/3515678725/ADR-002+Netbird+VPN+Replacement)
and the [Hybrid-ZTNA-Netbird Business Case](https://autoguru.atlassian.net/wiki/spaces/CS/pages/3515514895).

Netbird is the planned replacement for the Pritunl VPN. It is a true Layer-3 WireGuard VPN
and solves **native FQDN routing**: its Domain Resources (Networks) feature routes wildcard
FQDN traffic through a dedicated routing peer that carries a **static Elastic IP**. That IP
is added once to the Cloudflare origin allowlist and never changes (it survives EC2 Auto
Recovery), so Cloudflare-protected apps see a stable, managed egress instead of unmanageable
home ISP IPs.

Everything runs in the **autoguru-shared** account (`791686214595`), `ap-southeast-2`.

## Stacks

AWS CDK in **C#** (matching the rest of AutoGuru's CDK infrastructure), under `netbird/cdk`.
Two independent stacks (no "God" stack), each: dedicated VPC (public subnet + EIP, no NAT),
Amazon Linux 2023, Docker, IMDSv2 required, encrypted EBS, CloudWatch auto-recovery, an SSM-only
IAM role (no inbound SSH), and secrets read at boot from Secrets Manager. The EC2 user-data lives
in `netbird/scripts/*.sh` and is embedded into the assembly at build time.

| Stack | Instance | Purpose |
| --- | --- | --- |
| `NetbirdControlPlaneStack` | t3.small / 30 GB gp3 | Management, signal, relay, dashboard, Coturn |
| `NetbirdRoutingPeerStack` | t3.micro / 30 GB gp3 | Routing agent + WireGuard data plane + Cloudflare egress EIP |

## Prerequisites

1. **Entra ID app registration** `Netbird` (single tenant, SPA) already exists (COM-141):
client `5853144b-3c6f-4e39-a5b0-df1c3efcdcb1`, tenant `4542d3b9-a2ab-47a6-bc7a-1c25894c1adf`.
Its client secret must be in Secrets Manager (`ap-southeast-2`) at
`/netbird/control-plane/entra-client-secret`.
2. The routing-peer setup key secret `/netbird/routing-peer/setup-key` is created (empty) by
the routing-peer stack and populated after the control plane is set up.

## Deploy

Deploys run via the [`netbird-deploy`](../.github/workflows/netbird-deploy.yml) workflow:
pull requests touching `netbird/**` get a `cdk diff`; deploys are a manual `workflow_dispatch`
(`action: deploy`). The workflow assumes `AWS_DEPLOY_ROLE_ARN` in the shared account via OIDC.

Prerequisite: the shared account is already CDK-bootstrapped (the existing `SharedPlatformStack`
is deployed there via CDK), so no `cdk bootstrap` is needed.

Local (requires the .NET 9 SDK and the CDK CLI, with shared-account credentials):

```bash
cd netbird/cdk
dotnet build
npx cdk diff
npx cdk deploy NetbirdControlPlaneStack NetbirdRoutingPeerStack --require-approval never
```

## Post-deploy setup (manual, once)

1. From the `NetbirdControlPlaneStack` outputs, take the control-plane EIP and ask an admin to
create the Cloudflare DNS A record `netbird.autoguru.com.au` (DNS-only, not proxied) pointing
at it, then wait for propagation (Let's Encrypt needs the FQDN resolvable).
2. SSM into the control plane and run the Netbird setup (clone, `configure.sh`, `docker compose up`)
following the commented steps in [`scripts/control-plane-user-data.sh`](scripts/control-plane-user-data.sh).
This uses the external Entra OIDC flow, not the bundled ZITADEL script.
3. In the dashboard, create a setup key and store it in `/netbird/routing-peer/setup-key`, then
rebuild / reboot the routing peer so it enrols.
4. Define the Domain Resource (`*.autoguru.com.au`) and ask an admin to add the routing-peer EIP
(from `NetbirdRoutingPeerStack` outputs) to the Cloudflare origin allowlist.
5. Gate (COM-145): confirm one Cloudflare-protected FQDN egresses via the routing-peer EIP in the
Cloudflare access logs. Pritunl stays live until the gate passes.
21 changes: 21 additions & 0 deletions netbird/cdk/EmbeddedScript.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Reflection;

namespace Netbird.Cdk;

/// <summary>
/// Reads an EC2 user-data script that is embedded in this assembly (see the csproj
/// EmbeddedResource items). Embedding keeps the .sh files as real, LF-enforced files
/// while making them available at synth time without a working-directory dependency.
/// </summary>
internal static class EmbeddedScript
{
public static string Read(string logicalName)
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream(logicalName)
?? throw new InvalidOperationException(
$"Embedded user-data script '{logicalName}' was not found in the assembly.");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
29 changes: 29 additions & 0 deletions netbird/cdk/Netbird.Cdk.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Netbird.Cdk</RootNamespace>
<AssemblyName>Netbird.Cdk</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Amazon.CDK.Lib" Version="2.140.0" />
<PackageReference Include="Constructs" Version="[10.0.0,11.0.0)" />
</ItemGroup>

<!-- EC2 user-data bash scripts are embedded so they ship inside the assembly (no working-dir
dependency at synth time) while staying real .sh files: readable, lintable, and LF-enforced
via .gitattributes so a CRLF checkout on Windows cannot break the shebang on the instance. -->
<ItemGroup>
<EmbeddedResource Include="..\scripts\control-plane-user-data.sh">
<LogicalName>control-plane-user-data.sh</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="..\scripts\routing-peer-user-data.sh">
<LogicalName>routing-peer-user-data.sh</LogicalName>
</EmbeddedResource>
</ItemGroup>

</Project>
Loading
Loading