Harden Tailscale ACL policy with least-privilege grants
Replace permissive wildcard ACL with specific service grants: - Admin: full access to all services including NAS - Member: user-facing services only (no Grafana/Loki/NAS) - Infrastructure tags for device-to-device communication Add device tagging via Pulumi: - gilbert (workstation) - informational tag only - sifaka (NAS) - backup target for homelab, admin-only access - indri already tagged as homelab with service tags SSH hardening: - Remove root SSH access - Use "check" action with MFA for all SSH rules - Admin can SSH to homelab, workstation, nas, self Add ACL tests to validate policy behavior on save. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0918764e93
commit
7071293dda
2 changed files with 188 additions and 83 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue