Fix Tailscale ACL: use explicit emails instead of autogroups

Key learnings from debugging:
- autogroup:admin and dst: ["*"] don't work reliably in grants
- Tagging user-owned devices converts them to "tagged devices",
  losing user identity and breaking user-based SSH rules

Changes:
- Use blume.erich@gmail.com directly instead of autogroup:admin
- Use explicit tag destinations instead of wildcards
- Remove gilbert from Pulumi tagging (keep as user-owned device)
- Restore SSH with check action for MFA
- Add ACL tests for access validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-17 11:47:51 -08:00
commit 71358e455d
2 changed files with 39 additions and 81 deletions

View file

@ -55,17 +55,10 @@ indri_tags = tailscale.DeviceTags(
],
)
# 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
],
)
# 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
@ -86,8 +79,5 @@ 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)

View file

@ -1,9 +1,9 @@
// Tailnet ACL policy for tail8d86e.ts.net
// Managed by blumeops-pulumi - do not edit directly in Tailscale admin console
// Managed by blumeops-pulumi
{
// ============== Groups ==============
"groups": {
// Users with Jellyfin/media access (placeholder for future use)
// Placeholder for future Jellyfin media access
"group:allisonflix": [
"blume.erich@gmail.com",
"acmdavis@gmail.com",
@ -11,58 +11,56 @@
},
// ============== Access Grants ==============
// Principle of least privilege: grant only necessary access per service
// Note: autogroup:admin doesn't work reliably - use specific emails
// Note: dst: ["*"] doesn't work reliably - use explicit tags
"grants": [
// --- Admin: full access to everything ---
// --- Erich: full access to all infrastructure ---
{
"src": ["autogroup:admin"],
"dst": ["*"],
"src": ["blume.erich@gmail.com"],
"dst": ["tag:homelab", "tag:nas"],
"ip": ["*"],
},
{
"src": ["blume.erich@gmail.com"],
"dst": ["tag:grafana", "tag:kiwix", "tag:forge", "tag:devpi", "tag:loki", "tag:pg", "tag:feed"],
"ip": ["*"],
},
// --- Member access: user-facing services only ---
// Kiwix - offline Wikipedia (HTTPS)
// --- Members: user-facing services only ---
// Kiwix, Forge, devpi, Miniflux, PostgreSQL
{
"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)
// Note: No member access to grafana, loki, or NAS
// --- 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"],
@ -71,62 +69,38 @@
],
// ============== 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": [
// Admin can SSH to their own devices
// Members can SSH to their own devices
{
"src": ["autogroup:admin"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot"],
"action": "check",
"action": "check",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot"],
},
// Erich can SSH to homelab (for ansible)
{
"action": "check",
"src": ["blume.erich@gmail.com"],
"dst": ["tag:homelab"],
"users": ["autogroup:nonroot"],
"checkPeriod": "12h0m0s",
},
// Admin can SSH to homelab servers
// Erich can SSH to NAS
{
"src": ["autogroup:admin"],
"dst": ["tag:homelab"],
"users": ["autogroup:nonroot"],
"action": "check",
"action": "check",
"src": ["blume.erich@gmail.com"],
"dst": ["tag:nas"],
"users": ["autogroup:nonroot"],
"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": {
// Infrastructure management tag - applied by blumeops IaC
// Includes itself so the OAuth client can manage device tags
"tag:blumeops": ["autogroup:admin", "tag:blumeops"],
// Homelab servers - primary compute infrastructure (indri)
"tag:homelab": ["autogroup:admin", "tag:blumeops"],
// 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"],
"tag:forge": ["autogroup:admin", "tag:blumeops"],
@ -137,28 +111,22 @@
},
// ============== ACL Tests ==============
// Validate policy behavior - run on every save
"tests": [
// Admin (Erich) can access everything
// 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"],
"accept": ["tag:grafana:443", "tag:kiwix:443", "tag:feed:443", "tag:loki:3100", "tag:pg:5432", "tag:homelab:22"],
},
// 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
// 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
// Homelab can reach homelab and NAS
{
"src": "tag:homelab",
"accept": ["tag:homelab:22", "tag:homelab:443", "tag:nas:22", "tag:nas:445"],
"accept": ["tag:homelab:22", "tag:nas:445"],
},
],
}