diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index b6f8086..d038c89 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -26,3 +26,5 @@ tags: devpi_metrics - role: plex_metrics tags: plex_metrics + - role: tailscale_serve + tags: tailscale-serve diff --git a/ansible/roles/tailscale_serve/defaults/main.yml b/ansible/roles/tailscale_serve/defaults/main.yml new file mode 100644 index 0000000..8b1523d --- /dev/null +++ b/ansible/roles/tailscale_serve/defaults/main.yml @@ -0,0 +1,27 @@ +--- +# Tailscale serve configuration for this host +# Each service maps a Tailscale service name to local endpoints + +tailscale_services: + - name: svc:grafana + https: + port: 443 + upstream: http://localhost:3000 + + - name: svc:forge + https: + port: 443 + upstream: http://localhost:3001 + tcp: + port: 22 + upstream: tcp://localhost:2200 + + - name: svc:kiwix + https: + port: 443 + upstream: http://localhost:5501 + + - name: svc:pypi + https: + port: 443 + upstream: http://127.0.0.1:3141 diff --git a/ansible/roles/tailscale_serve/meta/main.yml b/ansible/roles/tailscale_serve/meta/main.yml new file mode 100644 index 0000000..904ced2 --- /dev/null +++ b/ansible/roles/tailscale_serve/meta/main.yml @@ -0,0 +1,6 @@ +--- +dependencies: + - role: grafana + - role: forgejo + - role: kiwix + - role: devpi diff --git a/ansible/roles/tailscale_serve/tasks/main.yml b/ansible/roles/tailscale_serve/tasks/main.yml new file mode 100644 index 0000000..6ed7442 --- /dev/null +++ b/ansible/roles/tailscale_serve/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Get current tailscale serve status + ansible.builtin.command: tailscale serve status --json + register: serve_status + changed_when: false + +- name: Configure HTTPS services + ansible.builtin.command: > + tailscale serve --service="{{ item.name }}" + --https={{ item.https.port }} {{ item.https.upstream }} + loop: "{{ tailscale_services }}" + when: item.https is defined + register: https_result + changed_when: "'already serving' not in https_result.stderr | default('')" + failed_when: false + +- name: Configure TCP services + ansible.builtin.command: > + tailscale serve --service="{{ item.name }}" + --tcp={{ item.tcp.port }} {{ item.tcp.upstream }} + loop: "{{ tailscale_services }}" + when: item.tcp is defined + register: tcp_result + changed_when: "'already serving' not in tcp_result.stderr | default('')" + failed_when: false diff --git a/mise-tasks/tailnet-preview b/mise-tasks/tailnet-preview new file mode 100755 index 0000000..e387ad5 --- /dev/null +++ b/mise-tasks/tailnet-preview @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +#MISE description="Preview tailnet changes with Pulumi" + +set -euo pipefail + +export TAILSCALE_OAUTH_CLIENT_ID=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get wi6bkf7bcccwfy4eu776ab4p4u --fields client_id) +export TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get wi6bkf7bcccwfy4eu776ab4p4u --fields client_secret --reveal) +export TAILSCALE_TAILNET="tail8d86e.ts.net" + +cd "$(dirname "$0")/../pulumi" +pulumi preview "$@" diff --git a/mise-tasks/tailnet-up b/mise-tasks/tailnet-up new file mode 100755 index 0000000..54fb798 --- /dev/null +++ b/mise-tasks/tailnet-up @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +#MISE description="Apply tailnet changes with Pulumi" + +set -euo pipefail + +export TAILSCALE_OAUTH_CLIENT_ID=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get wi6bkf7bcccwfy4eu776ab4p4u --fields client_id) +export TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get wi6bkf7bcccwfy4eu776ab4p4u --fields client_secret --reveal) +export TAILSCALE_TAILNET="tail8d86e.ts.net" + +cd "$(dirname "$0")/../pulumi" +pulumi up "$@" diff --git a/mise.toml b/mise.toml index cd1027a..2861f91 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,3 @@ [tools] "pipx:ansible-core" = { version = "latest", uvx = "true", uvx_args = "--with botocore --with boto3" } +pulumi = "latest" diff --git a/pulumi/.gitignore b/pulumi/.gitignore new file mode 100644 index 0000000..01a30d0 --- /dev/null +++ b/pulumi/.gitignore @@ -0,0 +1,10 @@ +# Python +.venv/ +__pycache__/ +*.py[cod] + +# uv +uv.lock + +# Pulumi +*.pyc diff --git a/pulumi/Pulumi.tail8d86e.yaml b/pulumi/Pulumi.tail8d86e.yaml new file mode 100644 index 0000000..712b360 --- /dev/null +++ b/pulumi/Pulumi.tail8d86e.yaml @@ -0,0 +1,2 @@ +config: + tailscale:tailnet: tail8d86e.ts.net diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000..ebfaed6 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,6 @@ +name: blumeops-tailnet +runtime: + name: python + options: + toolchain: uv +description: Tailnet configuration for tail8d86e.ts.net diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 0000000..8971cae --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,18 @@ +"""Pulumi program to manage tail8d86e.ts.net tailnet configuration.""" + +import pulumi +import pulumi_tailscale as tailscale +from pathlib import Path + +# Read the HuJSON policy file +policy_path = Path(__file__).parent / "policy.hujson" +policy_content = policy_path.read_text() + +# Manage the ACL - this completely overwrites the tailnet's ACL policy +acl = tailscale.Acl( + "tailnet-acl", + acl=policy_content, +) + +# Export useful info +pulumi.export("acl_id", acl.id) diff --git a/pulumi/policy.hujson b/pulumi/policy.hujson new file mode 100644 index 0000000..64bc11d --- /dev/null +++ b/pulumi/policy.hujson @@ -0,0 +1,104 @@ +// Example/default ACLs for unrestricted connections. +{ + // Declare static groups of users. Use autogroups for all users or users with a specific role. + // "groups": { + // "group:example": ["alice@example.com", "bob@example.com"], + // }, + + // Define the tags which can be applied to devices and by which users. + // "tagOwners": { + // "tag:example": ["autogroup:admin"], + // }, + + // Define grants that govern access for users, groups, autogroups, tags, + // Tailscale IP addresses, and subnet ranges. + "grants": [ + // Allow all connections. + // Comment this section out if you want to define specific restrictions. + { + "src": ["*"], + "dst": ["*"], + "ip": ["*"], + }, + + // Allow users in "group:example" to access "tag:example", but only from + // devices that are running macOS and have enabled Tailscale client auto-updating. + // {"src": ["group:example"], "dst": ["tag:example"], "ip": ["*"], "srcPosture":["posture:autoUpdateMac"]}, + ], + + // Define postures that will be applied to all rules without any specific + // srcPosture definition. + // "defaultSrcPosture": [ + // "posture:anyMac", + // ], + + // Define device posture rules requiring devices to meet + // certain criteria to access parts of your system. + // "postures": { + // // Require devices running macOS, a stable Tailscale + // // version and auto update enabled for Tailscale. + // "posture:autoUpdateMac": [ + // "node:os == 'macos'", + // "node:tsReleaseTrack == 'stable'", + // "node:tsAutoUpdate", + // ], + // // Require devices running macOS and a stable + // // Tailscale version. + // "posture:anyMac": [ + // "node:os == 'macos'", + // "node:tsReleaseTrack == 'stable'", + // ], + // }, + + // Define users and devices that can use Tailscale SSH. + "ssh": [ + // Allow all users to SSH into their own devices in check mode. + // Comment this section out if you want to define specific restrictions. + { + "action": "check", + "src": ["autogroup:member"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot", "root"], + }, + // Allow Erich to ssh on to the homelab server. + { + "src": ["blume.erich@gmail.com"], + "dst": ["tag:homelab"], + "users": ["autogroup:nonroot"], + "action": "check", + "checkPeriod": "12h0m0s", + }, + ], + + "tagOwners": { + // Grafana service host tag + "tag:grafana": ["autogroup:admin"], + + // This tag applies to instances which are meant to be accessible in my homelab. These instances can be SSH'ed in to by any member of the admin autogroup. + "tag:homelab": ["autogroup:admin"], + + // Kiwix, a local wiki server. I use it to create mirrors of wikipedia. + "tag:kiwix": ["autogroup:admin"], + + // Service tag for forgejo, scm host and code forge + "tag:forge": ["autogroup:admin"], + + // devpi pypi index + "tag:devpi": ["autogroup:admin"], + + // Loki log collection + "tag:loki": ["autogroup:admin"], + + // This tag is applied to resources modified by blumeops-pulumi IaC + "tag:blumeops": ["autogroup:admin"], + }, + + // Test access rules every time they're saved. + // "tests": [ + // { + // "src": "alice@example.com", + // "accept": ["tag:example"], + // "deny": ["100.101.102.103:443"], + // }, + // ], +} diff --git a/pulumi/pyproject.toml b/pulumi/pyproject.toml new file mode 100644 index 0000000..9b7d4aa --- /dev/null +++ b/pulumi/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "blumeops-tailnet" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "pulumi>=3.0.0", + "pulumi-tailscale>=0.24.0", +]