diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 8c5cd39..f8d4dde 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,42 @@ indri_tags = tailscale.DeviceTags( "tag:loki", "tag:pg", "tag:feed", - "tag:blumeops", ], ) -# Export useful info +# gilbert - MacBook Air M4, primary development workstation +# Can SSH to homelab for ansible provisioning and access observability tools +gilbert = tailscale.get_device(name="gilbert.tail8d86e.ts.net") +gilbert_tags = tailscale.DeviceTags( + "gilbert-tags", + device_id=gilbert.node_id, + tags=[ + "tag:workstation", # Workstation role - can SSH to homelab, access grafana/loki + "tag:blumeops", # Managed by this IaC + ], +) + +# 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("gilbert_device_id", gilbert.node_id) +pulumi.export("gilbert_tags", gilbert_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..3178f6c 100644 --- a/pulumi/policy.hujson +++ b/pulumi/policy.hujson @@ -1,111 +1,164 @@ -// Example/default ACLs for unrestricted connections. +// Tailnet ACL policy for tail8d86e.ts.net +// Managed by blumeops-pulumi - do not edit directly in Tailscale admin console { - // 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": { + // Users with Jellyfin/media access (placeholder for future use) + "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 ============== + // Principle of least privilege: grant only necessary access per service "grants": [ - // Allow all connections. - // Comment this section out if you want to define specific restrictions. + // --- Admin: full access to everything --- { - "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"]}, + // --- Member access: user-facing services only --- + // Kiwix - offline Wikipedia (HTTPS) + { + "src": ["autogroup:member"], + "dst": ["tag:kiwix"], + "ip": ["tcp:443"], + }, + // Forge - git web UI (HTTPS) and SSH + { + "src": ["autogroup:member"], + "dst": ["tag:forge"], + "ip": ["tcp:443", "tcp:22"], + }, + // devpi - PyPI proxy (HTTPS) + { + "src": ["autogroup:member"], + "dst": ["tag:devpi"], + "ip": ["tcp:443"], + }, + // Miniflux - RSS reader (HTTPS) + { + "src": ["autogroup:member"], + "dst": ["tag:feed"], + "ip": ["tcp:443"], + }, + // PostgreSQL - database + { + "src": ["autogroup:member"], + "dst": ["tag:pg"], + "ip": ["tcp:5432"], + }, + // Note: NAS access is admin-only (no member grant) + + // --- Infrastructure --- + // Note: tag:workstation exists for informational purposes only. + // Workstation access is handled via user membership (admin/member). + // Homelab servers can reach each other + { + "src": ["tag:homelab"], + "dst": ["tag:homelab"], + "ip": ["*"], + }, + // Homelab can reach NAS for backups + { + "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 ============== + // Note: Members have NO SSH access (removed autogroup:self rule) + // Note: SSH dst cannot use "*" - must use specific tags or autogroup:self "ssh": [ - // Allow all users to SSH into their own devices in check mode. - // Comment this section out if you want to define specific restrictions. + // Admin can SSH to their own devices { - "action": "check", - "src": ["autogroup:member"], - "dst": ["autogroup:self"], - "users": ["autogroup:nonroot", "root"], + "src": ["autogroup:admin"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot"], + "action": "check", + "checkPeriod": "12h0m0s", }, - // Allow Erich to ssh on to the homelab server. + // Admin can SSH to homelab servers { - "src": ["blume.erich@gmail.com"], + "src": ["autogroup:admin"], "dst": ["tag:homelab"], "users": ["autogroup:nonroot"], "action": "check", "checkPeriod": "12h0m0s", }, + // Admin can SSH to workstations + { + "src": ["autogroup:admin"], + "dst": ["tag:workstation"], + "users": ["autogroup:nonroot"], + "action": "check", + "checkPeriod": "12h0m0s", + }, + // Admin can SSH to NAS + { + "src": ["autogroup:admin"], + "dst": ["tag:nas"], + "users": ["autogroup:nonroot"], + "action": "check", + "checkPeriod": "12h0m0s", + }, + // Note: Device-to-device SSH (tag:workstation -> tag:homelab) not possible with + // "check" action. Use admin user SSH rules above for human-initiated ansible. ], + // ============== Tag Owners ============== + // Define who can assign each tag to devices "tagOwners": { - // Grafana service host tag - "tag:grafana": ["autogroup:admin", "tag:blumeops"], + // Infrastructure management tag - applied by blumeops IaC + // Includes itself so the OAuth client can manage device tags + "tag:blumeops": ["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. + // Homelab servers - primary compute infrastructure (indri) "tag:homelab": ["autogroup:admin", "tag:blumeops"], - // Kiwix, a local wiki server. I use it to create mirrors of wikipedia. + // Development workstations - can provision and manage homelab (gilbert) + "tag:workstation": ["autogroup:admin", "tag:blumeops"], + + // Network-attached storage devices (sifaka) + "tag:nas": ["autogroup:admin", "tag:blumeops"], + + // Service-specific tags for fine-grained access control + "tag:grafana": ["autogroup:admin", "tag:blumeops"], "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"], }, - // Test access rules every time they're saved. - // "tests": [ - // { - // "src": "alice@example.com", - // "accept": ["tag:example"], - // "deny": ["100.101.102.103:443"], - // }, - // ], + // ============== ACL Tests ============== + // Validate policy behavior - run on every save + "tests": [ + // Admin (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"], + }, + // Admin can SSH to homelab + { + "src": "blume.erich@gmail.com", + "accept": ["tag:homelab:22"], + }, + // Member (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 servers can communicate with each other and NAS + { + "src": "tag:homelab", + "accept": ["tag:homelab:22", "tag:homelab:443", "tag:nas:22", "tag:nas:445"], + }, + ], }