Add Gandi DNS management via Pulumi (#54)
## Summary - Restructure Pulumi into separate projects: `pulumi/tailscale/` and `pulumi/gandi/` - Add Gandi LiveDNS management for `eblu.me` domain - Create wildcard DNS record `*.ops.eblu.me` → indri's Tailscale IP (100.98.163.89) - Add mise tasks: `dns-up`, `dns-preview` - Update `tailnet-up` to pass `--yes` by default - Document PAT cycling process (expires every 30 days) ## Background This enables using real DNS names (`*.ops.eblu.me`) that resolve to Tailscale IPs, which allows containers and other systems to resolve services without depending on MagicDNS. Since Tailscale IPs (100.x.x.x) are not publicly routable, services remain tailnet-only while using standard DNS. ## Deployment and Testing - [ ] Run `cd pulumi/gandi && uv sync` to install dependencies - [ ] Run `cd pulumi/gandi && pulumi stack init eblu-me` to create stack - [ ] Run `mise run dns-preview` to verify configuration - [ ] Run `mise run dns-up` to apply DNS records - [ ] Verify with `dig +short test.ops.eblu.me` returns `100.98.163.89` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/54
This commit is contained in:
parent
af3536bc17
commit
b08faa50cc
17 changed files with 466 additions and 3 deletions
10
pulumi/tailscale/.gitignore
vendored
Normal file
10
pulumi/tailscale/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Python
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# uv
|
||||
uv.lock
|
||||
|
||||
# Pulumi
|
||||
*.pyc
|
||||
3
pulumi/tailscale/Pulumi.tail8d86e.yaml
Normal file
3
pulumi/tailscale/Pulumi.tail8d86e.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
config:
|
||||
tailscale:tailnet: tail8d86e.ts.net
|
||||
7
pulumi/tailscale/Pulumi.yaml
Normal file
7
pulumi/tailscale/Pulumi.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: blumeops-tailnet
|
||||
runtime:
|
||||
name: python
|
||||
options:
|
||||
toolchain: uv
|
||||
description: Tailnet configuration for tail8d86e.ts.net
|
||||
81
pulumi/tailscale/__main__.py
Normal file
81
pulumi/tailscale/__main__.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""Pulumi program to manage tail8d86e.ts.net tailnet configuration.
|
||||
|
||||
This program manages:
|
||||
- ACL policy (grants, SSH rules, tag owners, tests)
|
||||
- Device tags for infrastructure classification
|
||||
|
||||
Devices are tagged based on their role:
|
||||
- tag:homelab - Server infrastructure (indri)
|
||||
- tag:workstation - Development machines that can manage homelab (gilbert)
|
||||
- tag:nas - Network-attached storage (sifaka)
|
||||
- tag:blumeops - Resources managed by this IaC
|
||||
- Service tags (grafana, forge, etc.) - Fine-grained service access control
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
||||
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()
|
||||
|
||||
# Compute policy hash for change tracking
|
||||
policy_hash = hashlib.sha256(policy_content.encode()).hexdigest()[:12]
|
||||
|
||||
# Manage the ACL - this completely overwrites the tailnet's ACL policy
|
||||
acl = tailscale.Acl(
|
||||
"tailnet-acl",
|
||||
acl=policy_content,
|
||||
)
|
||||
|
||||
# ============== Device Tags ==============
|
||||
# Manage tags for devices in the tailnet.
|
||||
# Tags control access via the ACL policy in policy.hujson.
|
||||
|
||||
# indri - Mac Mini M1, primary homelab server
|
||||
# Hosts forge, loki, zot registry, and the k8s control plane.
|
||||
# Other services (grafana, kiwix, devpi, etc.) run in k8s with their own Tailscale devices.
|
||||
indri = tailscale.get_device(name="indri.tail8d86e.ts.net")
|
||||
indri_tags = tailscale.DeviceTags(
|
||||
"indri-tags",
|
||||
device_id=indri.node_id,
|
||||
tags=[
|
||||
"tag:homelab", # Server role - allows SSH from workstations
|
||||
"tag:blumeops", # Managed by this IaC
|
||||
# Service tags for services still hosted directly on indri
|
||||
"tag:forge",
|
||||
"tag:loki",
|
||||
"tag:registry", # Zot container registry
|
||||
"tag:k8s-api", # Kubernetes API server (minikube)
|
||||
],
|
||||
)
|
||||
|
||||
# NOTE: gilbert (MacBook Air M4) is NOT tagged via Pulumi
|
||||
# Tagging a user-owned device converts it to a "tagged device" which loses
|
||||
# user identity, breaking user-based SSH rules. gilbert remains user-owned
|
||||
# so blume.erich@gmail.com can SSH to homelab via the ACL rules.
|
||||
|
||||
# sifaka - Synology NAS, backup target
|
||||
# Homelab and workstations can access for backups
|
||||
sifaka = tailscale.get_device(name="sifaka.tail8d86e.ts.net")
|
||||
sifaka_tags = tailscale.DeviceTags(
|
||||
"sifaka-tags",
|
||||
device_id=sifaka.node_id,
|
||||
tags=[
|
||||
"tag:nas", # NAS role - accessible by homelab and workstations
|
||||
"tag:blumeops", # Managed by this IaC
|
||||
],
|
||||
)
|
||||
|
||||
# ============== Exports ==============
|
||||
pulumi.export("acl_id", acl.id)
|
||||
pulumi.export("policy_hash", policy_hash)
|
||||
|
||||
pulumi.export("indri_device_id", indri.node_id)
|
||||
pulumi.export("indri_tags", indri_tags.tags)
|
||||
|
||||
pulumi.export("sifaka_device_id", sifaka.node_id)
|
||||
pulumi.export("sifaka_tags", sifaka_tags.tags)
|
||||
170
pulumi/tailscale/policy.hujson
Normal file
170
pulumi/tailscale/policy.hujson
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// Tailnet ACL policy for tail8d86e.ts.net
|
||||
// Managed by blumeops-pulumi
|
||||
{
|
||||
// ============== Groups ==============
|
||||
"groups": {
|
||||
// Placeholder for future Jellyfin media access
|
||||
"group:allisonflix": [
|
||||
"blume.erich@gmail.com",
|
||||
"acmdavis@gmail.com",
|
||||
],
|
||||
},
|
||||
|
||||
// ============== Access Grants ==============
|
||||
"grants": [
|
||||
// --- Admins: full access to all infrastructure ---
|
||||
{
|
||||
"src": ["autogroup:admin"],
|
||||
"dst": ["*"],
|
||||
"ip": ["*"],
|
||||
},
|
||||
|
||||
// --- Members: user-facing services only ---
|
||||
// Kiwix, Forge, devpi, Miniflux, PostgreSQL
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:kiwix"],
|
||||
"ip": ["tcp:443"],
|
||||
},
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:forge"],
|
||||
"ip": ["tcp:443", "tcp:22"],
|
||||
},
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:devpi"],
|
||||
"ip": ["tcp:443"],
|
||||
},
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:feed"],
|
||||
"ip": ["tcp:443"],
|
||||
},
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:pg"],
|
||||
"ip": ["tcp:5432"],
|
||||
},
|
||||
// Note: No member access to grafana, loki, or NAS
|
||||
|
||||
// --- Infrastructure ---
|
||||
{
|
||||
"src": ["tag:homelab"],
|
||||
"dst": ["tag:homelab"],
|
||||
"ip": ["*"],
|
||||
},
|
||||
{
|
||||
"src": ["tag:homelab"],
|
||||
"dst": ["tag:nas"],
|
||||
"ip": ["*"],
|
||||
},
|
||||
|
||||
// --- CI Gateway ---
|
||||
// Ephemeral CI containers can push images to registry
|
||||
{
|
||||
"src": ["tag:ci-gateway"],
|
||||
"dst": ["tag:registry"],
|
||||
"ip": ["tcp:443"],
|
||||
},
|
||||
|
||||
// --- Kubernetes workloads ---
|
||||
// k8s workloads (e.g., Woodpecker CI) can push/pull from registry
|
||||
{
|
||||
"src": ["tag:k8s"],
|
||||
"dst": ["tag:registry"],
|
||||
"ip": ["tcp:443"],
|
||||
},
|
||||
// k8s workloads (e.g., ArgoCD) can access forge on indri for GitOps
|
||||
// HTTP on 3001, SSH on 2200
|
||||
{
|
||||
"src": ["tag:k8s"],
|
||||
"dst": ["tag:homelab"],
|
||||
"ip": ["tcp:3001", "tcp:2200"],
|
||||
},
|
||||
|
||||
// Homelab can reach k8s services: PostgreSQL, CNPG metrics, Prometheus/Loki
|
||||
{
|
||||
"src": ["tag:homelab"],
|
||||
"dst": ["tag:k8s"],
|
||||
"ip": ["tcp:443", "tcp:5432", "tcp:9187"],
|
||||
},
|
||||
],
|
||||
|
||||
// ============== SSH Access ==============
|
||||
"ssh": [
|
||||
// Members can SSH to their own devices
|
||||
{
|
||||
"action": "check",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"users": ["autogroup:nonroot"],
|
||||
},
|
||||
// Admins can SSH to homelab (for ansible)
|
||||
{
|
||||
"action": "check",
|
||||
"src": ["autogroup:admin"],
|
||||
"dst": ["tag:homelab"],
|
||||
"users": ["autogroup:nonroot"],
|
||||
"checkPeriod": "12h0m0s",
|
||||
},
|
||||
// Admins can SSH to NAS
|
||||
{
|
||||
"action": "check",
|
||||
"src": ["autogroup:admin"],
|
||||
"dst": ["tag:nas"],
|
||||
"users": ["autogroup:nonroot"],
|
||||
"checkPeriod": "12h0m0s",
|
||||
},
|
||||
],
|
||||
|
||||
// ============== Tag Owners ==============
|
||||
"tagOwners": {
|
||||
"tag:blumeops": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:homelab": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:workstation": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:nas": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:grafana": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:kiwix": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:forge": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:devpi": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:loki": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:pg": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:feed": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:registry": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:k8s-api": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:k8s-operator": ["autogroup:admin", "tag:blumeops"],
|
||||
"tag:k8s": ["autogroup:admin", "tag:blumeops", "tag:k8s-operator"],
|
||||
"tag:ci-gateway": ["autogroup:admin", "tag:blumeops"],
|
||||
},
|
||||
|
||||
// ============== ACL Tests ==============
|
||||
"tests": [
|
||||
// Erich can access everything
|
||||
{
|
||||
"src": "blume.erich@gmail.com",
|
||||
"accept": ["tag:grafana:443", "tag:kiwix:443", "tag:feed:443", "tag:loki:3100", "tag:pg:5432", "tag:homelab:22", "tag:registry:443", "tag:k8s-api:443"],
|
||||
},
|
||||
// Allison can access user services but NOT grafana, loki, or NAS
|
||||
{
|
||||
"src": "acmdavis@gmail.com",
|
||||
"accept": ["tag:kiwix:443", "tag:forge:443", "tag:feed:443", "tag:pg:5432"],
|
||||
"deny": ["tag:grafana:443", "tag:loki:3100", "tag:nas:445", "tag:registry:443", "tag:k8s-api:443"],
|
||||
},
|
||||
// Homelab can reach homelab, NAS, and k8s services (postgres, metrics, prometheus/loki)
|
||||
{
|
||||
"src": "tag:homelab",
|
||||
"accept": ["tag:homelab:22", "tag:nas:445", "tag:k8s:443", "tag:k8s:5432", "tag:k8s:9187"],
|
||||
},
|
||||
// K8s workloads can reach registry and forge (on indri:3001 HTTP, :2200 SSH)
|
||||
{
|
||||
"src": "tag:k8s",
|
||||
"accept": ["tag:registry:443", "tag:homelab:3001", "tag:homelab:2200"],
|
||||
},
|
||||
// CI gateway can push to registry
|
||||
{
|
||||
"src": "tag:ci-gateway",
|
||||
"accept": ["tag:registry:443"],
|
||||
},
|
||||
],
|
||||
}
|
||||
5
pulumi/tailscale/pyproject.toml
Normal file
5
pulumi/tailscale/pyproject.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "blumeops-tailnet"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["pulumi>=3.0.0", "pulumi-tailscale>=0.24.0"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue