blumeops/docs/how-to/update-tailscale-acls.md
Erich Blume b197bd5f58 Adopt Dagger CI for docs build (Phase 2) (#157)
## Summary

Migrates the docs build pipeline to Dagger (Phase 2 of the Dagger CI adoption plan).

- **Backfill `date-modified` frontmatter** on all 80 docs — Dagger's `--src=.` excludes `.git`, so Quartz can't use git history for page dates. Frontmatter dates work with or without git.
- **New `docs-check-frontmatter` mise task + pre-commit hook** — validates all docs have `title`, `tags`, and `date-modified`
- **New Dagger functions** — `build_changelog` (towncrier in Python container) and `build_docs` (chains changelog → Quartz build in Node container, returns tarball)
- **Simplified CI workflow** — the ~44-line inline Quartz build (clone, npm ci, build, tar, cleanup) is replaced by `dagger call build-docs`. Changelog step remains local on the runner since towncrier needs to modify the host working tree for the git commit.

### Design decisions

- **Towncrier runs twice in CI**: once inside Dagger (for the docs tarball) and once on the runner (for the git commit). This is intentional — Dagger's directory export is additive and can't delete the consumed changelog fragments from the host.
- **Artifact hosting stays on Forgejo Releases** (not migrated to Forgejo Packages as the plan doc originally suggested). That migration can happen independently.
- **`date-modified` frontmatter** preserved even though `build_changelog` installs git — the git there is only for towncrier's `git add` call, not for history. The local iteration story (`dagger call build-docs --src=. --version=dev` with uncommitted changes) depends on frontmatter dates.

### Local iteration

```bash
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz
tar tf docs-dev.tar.gz | head -20
```

## Deployment and Testing

- [x] `dagger call build-docs --src=. --version=dev` produces valid 1.1MB tarball (149 HTML pages)
- [x] Pre-commit hooks pass (including new `docs-check-frontmatter`)
- [ ] Full `workflow_dispatch` run after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/157
2026-02-11 16:33:16 -08:00

2 KiB

title date-modified tags
Update Tailscale ACLs 2026-02-07
how-to
tailscale
pulumi

Update Tailscale ACLs

How to modify Tailscale access control policies for the tailnet.

Prerequisites

  • Pulumi CLI installed (brew install pulumi)
  • Access to 1Password blumeops vault (for OAuth credentials)

Edit the Policy

The ACL policy lives in pulumi/policy.hujson (HuJSON format with comments).

Common changes:

Add a new ACL rule

{
  "acls": [
    // ... existing rules ...
    {
      "action": "accept",
      "src": ["autogroup:admin"],
      "dst": ["tag:newservice:*"]
    }
  ]
}

Add a new tag

{
  "tagOwners": {
    // ... existing tags ...
    "tag:newservice": ["autogroup:admin"]
  }
}

Add a new group

{
  "groups": {
    // ... existing groups ...
    "group:newgroup": ["user1@example.com", "user2@example.com"]
  }
}

Preview and Apply

# Preview changes (always do this first)
mise run tailnet-preview

# Apply changes
mise run tailnet-up

# Skip confirmation prompt
mise run tailnet-up -- --yes

Verify

Check the Tailscale admin console at https://login.tailscale.com/ to confirm changes.

Common Patterns

Service-specific access

Grant access to a specific service port:

{
  "action": "accept",
  "src": ["group:users"],
  "dst": ["tag:homelab:8080"]
}

SSH access

{
  "ssh": [
    {
      "action": "check",
      "src": ["autogroup:admin"],
      "dst": ["tag:servers"],
      "users": ["autogroup:nonroot"]
    }
  ]
}

All ports for admins

{
  "action": "accept",
  "src": ["autogroup:admin"],
  "dst": ["*:*"]
}

Troubleshooting

"Credential expired" error: Re-authenticate Pulumi with Tailscale. The OAuth token may need refreshing.

Changes not taking effect: ACL changes are applied immediately. If a device isn't following new rules, try tailscale down && tailscale up on that device.