diff --git a/mise-tasks/dns-preview b/mise-tasks/dns-preview new file mode 100755 index 0000000..7d7578e --- /dev/null +++ b/mise-tasks/dns-preview @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +#MISE description="Preview DNS changes to eblu.me with Pulumi" + +set -euo pipefail + +GANDI_PERSONAL_ACCESS_TOKEN=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mco6ka3dc3rmw7zkg2dhia5d2m --fields pat --reveal) +export GANDI_PERSONAL_ACCESS_TOKEN + +cd "$(dirname "$0")/../pulumi/gandi" +pulumi preview "$@" diff --git a/mise-tasks/dns-up b/mise-tasks/dns-up new file mode 100755 index 0000000..a0d3849 --- /dev/null +++ b/mise-tasks/dns-up @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +#MISE description="Apply DNS changes to eblu.me with Pulumi" + +set -euo pipefail + +GANDI_PERSONAL_ACCESS_TOKEN=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mco6ka3dc3rmw7zkg2dhia5d2m --fields pat --reveal) +export GANDI_PERSONAL_ACCESS_TOKEN + +cd "$(dirname "$0")/../pulumi/gandi" +pulumi up --yes "$@" diff --git a/mise-tasks/tailnet-preview b/mise-tasks/tailnet-preview index dd9e308..ceb6439 100755 --- a/mise-tasks/tailnet-preview +++ b/mise-tasks/tailnet-preview @@ -9,5 +9,5 @@ TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w export TAILSCALE_OAUTH_CLIENT_SECRET export TAILSCALE_TAILNET="tail8d86e.ts.net" -cd "$(dirname "$0")/../pulumi" +cd "$(dirname "$0")/../pulumi/tailscale" pulumi preview "$@" diff --git a/mise-tasks/tailnet-up b/mise-tasks/tailnet-up index 4b097b6..f22048b 100755 --- a/mise-tasks/tailnet-up +++ b/mise-tasks/tailnet-up @@ -9,5 +9,5 @@ TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w export TAILSCALE_OAUTH_CLIENT_SECRET export TAILSCALE_TAILNET="tail8d86e.ts.net" -cd "$(dirname "$0")/../pulumi" -pulumi up "$@" +cd "$(dirname "$0")/../pulumi/tailscale" +pulumi up --yes "$@" diff --git a/pulumi/gandi/.gitignore b/pulumi/gandi/.gitignore new file mode 100644 index 0000000..21d0b89 --- /dev/null +++ b/pulumi/gandi/.gitignore @@ -0,0 +1 @@ +.venv/ diff --git a/pulumi/gandi/Pulumi.eblu-me.yaml b/pulumi/gandi/Pulumi.eblu-me.yaml new file mode 100644 index 0000000..e5f86c4 --- /dev/null +++ b/pulumi/gandi/Pulumi.eblu-me.yaml @@ -0,0 +1,6 @@ +--- +config: + blumeops-dns:domain: eblu.me + blumeops-dns:subdomain: ops + # indri's Tailscale IP - only routable within tailnet + blumeops-dns:tailscale_ip: "100.98.163.89" diff --git a/pulumi/gandi/Pulumi.yaml b/pulumi/gandi/Pulumi.yaml new file mode 100644 index 0000000..81e7215 --- /dev/null +++ b/pulumi/gandi/Pulumi.yaml @@ -0,0 +1,7 @@ +--- +name: blumeops-dns +runtime: + name: python + options: + toolchain: uv +description: DNS configuration for eblu.me via Gandi LiveDNS diff --git a/pulumi/gandi/README.md b/pulumi/gandi/README.md new file mode 100644 index 0000000..469f589 --- /dev/null +++ b/pulumi/gandi/README.md @@ -0,0 +1,92 @@ +# Gandi DNS Management + +This Pulumi project manages DNS records for `eblu.me` via Gandi LiveDNS. + +## What It Does + +Creates DNS records that point `*.ops.eblu.me` to indri's Tailscale IP (`100.98.163.89`). + +Since Tailscale IPs (100.x.x.x) are not routable on the public internet, these +DNS records effectively make services accessible only from within the tailnet, +while still using real, resolvable DNS names. + +## Setup + +```bash +cd pulumi/gandi +uv sync +pulumi stack select eblu-me # or: pulumi stack init eblu-me +``` + +## Authentication + +This project requires a Gandi Personal Access Token (PAT) with LiveDNS permissions. + +**The PAT expires every 30 days and must be cycled manually.** + +### Cycling the PAT + +1. Go to [Gandi PAT Management](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat) + +2. Create a new PAT: + - Name: `blumeops-pulumi` (or similar) + - Expiration: 30 days (maximum) + - Permissions: **Manage domain name technical configurations** (under Domains) + +3. Update 1Password: + ```bash + # Update the existing item with the new PAT value + op item edit mco6ka3dc3rmw7zkg2dhia5d2m pat="" --vault vg6xf6vvfmoh5hqjjhlhbeoaie + ``` + +4. Delete the old PAT from Gandi admin console + +### Running with Authentication + +The mise task handles fetching the PAT from 1Password: + +```bash +mise run dns-up # Preview and apply changes +mise run dns-preview # Preview only +``` + +Or manually: + +```bash +export GANDI_PERSONAL_ACCESS_TOKEN=$(op item get mco6ka3dc3rmw7zkg2dhia5d2m --field pat --reveal --vault vg6xf6vvfmoh5hqjjhlhbeoaie) +pulumi up +``` + +## DNS Records Created + +| Record | Type | Value | Purpose | +|--------|------|-------|---------| +| `*.ops.eblu.me` | A | 100.98.163.89 | Wildcard for all services | +| `ops.eblu.me` | A | 100.98.163.89 | Base subdomain | + +## Service Hostnames + +Once Caddy is configured, services will be accessible at: + +- `forge.ops.eblu.me` - Forgejo git server +- `registry.ops.eblu.me` - Zot container registry +- `grafana.ops.eblu.me` - Grafana dashboards +- `argocd.ops.eblu.me` - ArgoCD +- `feed.ops.eblu.me` - Miniflux RSS reader +- `pypi.ops.eblu.me` - DevPI Python index +- `kiwix.ops.eblu.me` - Kiwix offline content +- `tesla.ops.eblu.me` - TeslaMate +- `torrent.ops.eblu.me` - Transmission +- `prometheus.ops.eblu.me` - Prometheus metrics +- `loki.ops.eblu.me` - Loki logs + +## Changing the Target IP + +If indri's Tailscale IP changes, update `Pulumi.eblu-me.yaml`: + +```yaml +config: + blumeops-dns:tailscale_ip: "NEW_IP_HERE" +``` + +Then run `mise run dns-up` to apply. diff --git a/pulumi/gandi/__main__.py b/pulumi/gandi/__main__.py new file mode 100644 index 0000000..51679b6 --- /dev/null +++ b/pulumi/gandi/__main__.py @@ -0,0 +1,49 @@ +"""Pulumi program to manage eblu.me DNS via Gandi LiveDNS. + +This program manages DNS records for blumeops infrastructure: +- Wildcard record for *.ops.eblu.me pointing to indri's Tailscale IP +- This allows services to be accessed via real DNS names while remaining + tailnet-only (Tailscale IPs are not publicly routable) + +Authentication: + Set GANDI_PERSONAL_ACCESS_TOKEN environment variable. + See README.md for PAT management instructions. +""" + +import pulumi +import pulumiverse_gandi as gandi + +# Get configuration +config = pulumi.Config() +domain = config.require("domain") # eblu.me +subdomain = config.require("subdomain") # ops +tailscale_ip = config.require("tailscale_ip") # 100.98.163.89 + +# Wildcard A record for *.ops.eblu.me +# Points to indri's Tailscale IP, which is only routable within the tailnet. +# This allows containers and other systems to resolve real DNS names +# while keeping services private to the tailnet. +wildcard_record = gandi.livedns.Record( + "ops-wildcard", + zone=domain, + name=f"*.{subdomain}", + type="A", + ttl=300, + values=[tailscale_ip], +) + +# Base subdomain record (ops.eblu.me) - same IP +base_record = gandi.livedns.Record( + "ops-base", + zone=domain, + name=subdomain, + type="A", + ttl=300, + values=[tailscale_ip], +) + +# ============== Exports ============== +pulumi.export("domain", domain) +pulumi.export("wildcard_fqdn", f"*.{subdomain}.{domain}") +pulumi.export("base_fqdn", f"{subdomain}.{domain}") +pulumi.export("target_ip", tailscale_ip) diff --git a/pulumi/gandi/pyproject.toml b/pulumi/gandi/pyproject.toml new file mode 100644 index 0000000..472c93a --- /dev/null +++ b/pulumi/gandi/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "blumeops-dns" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = ["pulumi>=3.0.0", "pulumiverse-gandi>=2.3.0"] diff --git a/pulumi/.gitignore b/pulumi/tailscale/.gitignore similarity index 100% rename from pulumi/.gitignore rename to pulumi/tailscale/.gitignore diff --git a/pulumi/Pulumi.tail8d86e.yaml b/pulumi/tailscale/Pulumi.tail8d86e.yaml similarity index 100% rename from pulumi/Pulumi.tail8d86e.yaml rename to pulumi/tailscale/Pulumi.tail8d86e.yaml diff --git a/pulumi/Pulumi.yaml b/pulumi/tailscale/Pulumi.yaml similarity index 100% rename from pulumi/Pulumi.yaml rename to pulumi/tailscale/Pulumi.yaml diff --git a/pulumi/__main__.py b/pulumi/tailscale/__main__.py similarity index 100% rename from pulumi/__main__.py rename to pulumi/tailscale/__main__.py diff --git a/pulumi/policy.hujson b/pulumi/tailscale/policy.hujson similarity index 100% rename from pulumi/policy.hujson rename to pulumi/tailscale/policy.hujson diff --git a/pulumi/pyproject.toml b/pulumi/tailscale/pyproject.toml similarity index 100% rename from pulumi/pyproject.toml rename to pulumi/tailscale/pyproject.toml