Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions docs/iac-dns-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# IaC DNS providers

The `infra.dns` resource type is implemented by multiple plugins.
Each provider has different capabilities and auth requirements.

## Provider matrix

| Provider | Plugin | Auth | CIDR allowlist | Bulk ops | Status |
|----------|--------|------|----------------|----------|--------|
| DigitalOcean | [workflow-plugin-digitalocean](https://github.com/GoCodeAlone/workflow-plugin-digitalocean) | API token | n/a | yes (idempotent record list) | verified |
| Namecheap | [workflow-plugin-namecheap](https://github.com/GoCodeAlone/workflow-plugin-namecheap) | API user + key + single client IP | NO — single-IP only | yes (`setHosts` is full-replace) | experimental |
| Hover | [workflow-plugin-hover](https://github.com/GoCodeAlone/workflow-plugin-hover) | username + password + TOTP (browser-flow) | n/a | per-record (no batch API) | experimental |

## Configuration shape (all providers)

```yaml
modules:
- name: <provider-instance-name>
type: iac.provider.<digitalocean|namecheap|hover>
config:
Comment on lines +19 to +20
# provider-specific auth keys; see each plugin's README
...

- name: iac-state
type: iac.state
config:
backend: <memory|spaces|gcs|azureblob|postgres>

resources:
- name: <zone-id>
type: infra.dns
config:
provider: <provider-instance-name>
domain: example.com
records:
- type: A
name: '@' # apex, or subdomain
data: 203.0.113.10
ttl: 1800
- type: CNAME
name: www
data: example.com.
ttl: 1800
- type: MX
name: '@'
data: mail.example.com.
mx: 10 # MX priority
ttl: 3600
- type: TXT
name: '_acme-challenge'
data: "abc123"
ttl: 60
```

## Per-provider notes

### DigitalOcean

- Best for the GoCodeAlone reference stack (App Platform droplets
resolved by name; built-in DNS).
- Token must have full read+write scope.

### Namecheap

**Allowlist gotcha:** every IP that hits `api.namecheap.com` must be
explicitly whitelisted at Profile → Tools → Namecheap API Access.
Namecheap does NOT support CIDR. CI runners with rotating outbound
IPs need either:

1. A NAT gateway with a static egress IP.
2. A bastion host that proxies the API call.

The plugin's Config.Validate refuses CIDR strings outright so the
failure is detected at boot, not at apply time.

`setHosts` is a full-replace API: the plugin reads existing records
first and merges only the diff. Concurrent applies against the same
zone can lose writes — serialize with `wfctl infra apply`'s
single-pass guarantee.

### Hover

Hover has no official API. The plugin mimics the browser-side auth
used by [pjslauta/hover-dyn-dns](https://github.com/pjslauta/hover-dyn-dns):

1. GET `/signin` → parse CSRF `_token`.
2. POST `/signin` with username + password + token.
3. GET `/signin/totp` → fresh `_token`.
4. POST `/signin/totp` with RFC 6238 code + token.
5. Subsequent `/api/dns/...` requests carry the session cookie.

**TOTP**: provide the base32 seed (shown when you enabled 2FA in
Hover). Codes are generated in-process via pure-Go HMAC-SHA1.

**Failure modes**:

- Hover may serve a CAPTCHA challenge on suspicious logins. The
plugin doesn't solve CAPTCHAs; log in manually from the same IP
once to seed trust, OR use a static egress IP.
- Hover's signin HTML can change. The plugin fails loud with
`CSRF token not found at /signin` when the regex stops matching.
- No batch API — record edits are per-call. Don't use Hover for
zones with > ~50 records.

## Dynamic DNS

For dynamic IPs (home labs, mobile workstations), pair any DNS
provider with the `infra.dyndns` module:

```yaml
- name: home-dns
type: infra.dyndns
config:
provider: namecheap # any iac.provider.* module name
domain: gocodealone.tech
record_name: home
poll_interval: 5m
detect_via: [icanhazip, ifconfig.me, ipify]
quorum: 2 # 2-of-3 must agree before update fires
```

The daemon:

1. Polls each detector in parallel.
2. Requires `quorum` of them to return the same IP.
3. Compares to last-known IP.
4. Calls the provider's `UpdateRecord` on change.
5. Exponential backoff on consecutive failures (capped at 1h).

Per-record `detect_via` lets you trade redundancy for fewer
outbound calls; private LANs without internet access can supply
a custom detector via the plugin SDK.

## Secret management

Each provider declares its required secrets in plugin.json's
`required_secrets[]`. To set them all at once:

```sh
wfctl secrets setup --plugin workflow-plugin-namecheap \
--scope org --org GoCodeAlone

wfctl secrets setup --plugin workflow-plugin-hover \
--scope env --env production
```

See `docs/wfctl-secrets-scopes.md` for the scope flag matrix.

## Provider plan

The full DNS provider plan (Namecheap + Hover + dyndns + scoped
secret-set) is tracked in `docs/plans/2026-05-20-dns-providers.md`
(workflow#735) — caveman SPEC format with 20 tasks, 16 constraints,
18 invariants.
121 changes: 121 additions & 0 deletions docs/wfctl-secrets-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# wfctl secrets — GitHub scope reference

`wfctl secrets set` and `wfctl secrets setup --plugin` both write to
one of three GitHub secret destinations. Each requires a different
PAT scope and exposes different visibility controls.
Comment on lines +3 to +5

| Scope | URL prefix | PAT scopes | Visibility flags |
|-------|------------|-----------|------------------|
| `repo` (default) | `/repos/{owner}/{repo}/actions/secrets/{name}` | `repo` | — |
| `env` | `/repos/{owner}/{repo}/environments/{env}/secrets/{name}` | `repo`, `workflow` | — |
| `org` | `/orgs/{org}/actions/secrets/{name}` | `admin:org` | `--visibility {all,selected,private}` |

## Default (repo) scope

Backwards-compatible. Reads the repo from `secrets.config.repo` in
`app.yaml`:

```sh
wfctl secrets set MY_TOKEN --value "abc123"
# → PUT /repos/GoCodeAlone/example/actions/secrets/MY_TOKEN
```

Pipes are honored:

```sh
echo -n "abc123" | wfctl secrets set MY_TOKEN
```

If stdin is a TTY and neither `--value` nor `--from-file` is set,
the value is read with `term.ReadPassword` (masked).

## Environment scope

Writes to a repo's GitHub Actions environment. Requires the env to
already exist (create it once in the repo's Settings → Environments
panel).

```sh
wfctl secrets set STRIPE_KEY \
--scope env --env production \
--value "sk_live_..."
# → PUT /repos/GoCodeAlone/example/environments/production/secrets/STRIPE_KEY
```

Repo is still resolved from `app.yaml`'s `secrets.config.repo`.

## Organization scope

Writes a secret that any selected repo can pull. Bypasses `app.yaml`
since org secrets are out-of-band of repo config. The PAT in
`$GITHUB_TOKEN` (or `--token-env`) MUST carry `admin:org`.

```sh
# All repos in the org can pull this secret.
wfctl secrets set SHARED_API \
--scope org --org GoCodeAlone \
--visibility all \
--value "$(openssl rand -hex 32)"

# Only private + internal repos can pull.
wfctl secrets set INTERNAL_API \
--scope org --org GoCodeAlone \
--visibility private \
--value "..."

# Only the listed repo IDs can pull. (selected_repository_ids
# are populated programmatically via a follow-up; CLI accepts
# them in a future flag.)
wfctl secrets set CI_SECRET \
--scope org --org GoCodeAlone \
--visibility selected \
--value "..."
```

## Plugin-driven setup

If you're configuring a plugin that declares `required_secrets[]` in
its `plugin.json` (workflow-plugin-namecheap, workflow-plugin-hover,
etc.), use the interactive setup flow:

```sh
wfctl secrets setup --plugin workflow-plugin-hover \
--scope org --org GoCodeAlone --visibility all
```

This:

1. Reads `data/plugins/workflow-plugin-hover/plugin.json`.
2. Iterates `required_secrets[]`.
3. Prompts for each (masked iff `sensitive: true`).
4. Writes each to the chosen GH scope.

Pipe a value list to skip the prompt loop in CI:

```sh
printf 'alice\nhunter2\nJBSWY3DPEHPK3PXP\n' | \
wfctl secrets setup --plugin workflow-plugin-hover \
--scope org --org GoCodeAlone
```

## PAT scope cheat sheet

| Token use | Required scopes |
|-----------|-----------------|
| Repo secrets | `repo` (or fine-grained `Actions:write` + `Secrets:write`) |
| Env secrets | `repo` + `workflow` |
| Org secrets | `admin:org` (classic PAT) — fine-grained PATs cannot manage org secrets as of GH API v2022-11-28 |

## Troubleshooting

- **`HTTP 403: Resource not accessible by integration`** — missing
PAT scope. Most often `admin:org` for `--scope=org`.
- **`HTTP 422: secret value cannot be empty`** — the prompted value
was empty. The setup flow skips empty values; check terminal
echo settings.
- **`improperly encrypted secret`** — local clock skew vs GitHub or
a truncated public key. Re-run; the encryption nonce is per-call.
- **Org secret missing from a repo's Actions environment** — check
visibility. `selected` requires the repo ID to be in
`selected_repository_ids`; CLI accepts the list via a follow-up
flag.
Loading