diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 8c5cd39..33f3d0d 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,4 +1,18 @@ -"""Pulumi program to manage tail8d86e.ts.net tailnet configuration.""" +"""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 @@ -8,6 +22,9 @@ from pathlib import Path 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", @@ -15,15 +32,19 @@ acl = tailscale.Acl( ) # ============== Device Tags ============== -# Manage tags for devices in the tailnet +# Manage tags for devices in the tailnet. +# Tags control access via the ACL policy in policy.hujson. -# indri - Mac Mini M1 running homelab services +# indri - Mac Mini M1, primary homelab server +# Hosts all user-facing services (grafana, forge, kiwix, etc.) indri = tailscale.get_device(name="indri.tail8d86e.ts.net") indri_tags = tailscale.DeviceTags( "indri-tags", device_id=indri.node_id, tags=[ - "tag:homelab", + "tag:homelab", # Server role - allows SSH from workstations + "tag:blumeops", # Managed by this IaC + # Service tags - enable fine-grained access control per service "tag:grafana", "tag:forge", "tag:kiwix", @@ -31,11 +52,32 @@ indri_tags = tailscale.DeviceTags( "tag:loki", "tag:pg", "tag:feed", - "tag:blumeops", ], ) -# Export useful info +# 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) diff --git a/pulumi/policy.hujson b/pulumi/policy.hujson index 45ad401..d215ef7 100644 --- a/pulumi/policy.hujson +++ b/pulumi/policy.hujson @@ -1,111 +1,125 @@ -// Example/default ACLs for unrestricted connections. +// Tailnet ACL policy for tail8d86e.ts.net +// Managed by blumeops-pulumi { - // 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"], - // }, + // ============== Groups ============== + "groups": { + // Placeholder for future Jellyfin media access + "group:allisonflix": [ + "blume.erich@gmail.com", + "acmdavis@gmail.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. + // ============== Access Grants ============== "grants": [ - // Allow all connections. - // Comment this section out if you want to define specific restrictions. + // --- Admins: full access to all infrastructure --- { - "src": ["*"], + "src": ["autogroup:admin"], "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"]}, + // --- 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": ["*"], + }, ], - // 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 Access ============== "ssh": [ - // Allow all users to SSH into their own devices in check mode. - // Comment this section out if you want to define specific restrictions. + // Members can SSH to their own devices { "action": "check", "src": ["autogroup:member"], "dst": ["autogroup:self"], - "users": ["autogroup:nonroot", "root"], + "users": ["autogroup:nonroot"], }, - // Allow Erich to ssh on to the homelab server. + // Admins can SSH to homelab (for ansible) { - "src": ["blume.erich@gmail.com"], - "dst": ["tag:homelab"], - "users": ["autogroup:nonroot"], - "action": "check", + "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": { - // Grafana service host tag - "tag:grafana": ["autogroup:admin", "tag:blumeops"], - - // 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", "tag:blumeops"], - - // Kiwix, a local wiki server. I use it to create mirrors of wikipedia. - "tag:kiwix": ["autogroup:admin", "tag:blumeops"], - - // Service tag for forgejo, scm host and code forge - "tag:forge": ["autogroup:admin", "tag:blumeops"], - - // devpi pypi index - "tag:devpi": ["autogroup:admin", "tag:blumeops"], - - // Loki log collection - "tag:loki": ["autogroup:admin", "tag:blumeops"], - - // PostgreSQL database server - "tag:pg": ["autogroup:admin", "tag:blumeops"], - - // Miniflux RSS/Atom feed reader - "tag:feed": ["autogroup:admin", "tag:blumeops"], - - // This tag is applied to resources modified by blumeops-pulumi IaC - // Includes itself so the OAuth client can apply it to devices "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"], }, - // Test access rules every time they're saved. - // "tests": [ - // { - // "src": "alice@example.com", - // "accept": ["tag:example"], - // "deny": ["100.101.102.103:443"], - // }, - // ], + // ============== 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"], + }, + // 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"], + }, + // Homelab can reach homelab and NAS + { + "src": "tag:homelab", + "accept": ["tag:homelab:22", "tag:nas:445"], + }, + ], }