feat(dns): web-ssh, a DNS-only name for the web host #275

Merged
jfraeys merged 2 commits from jfraeys/feat/web-ssh-dns into main 2026-10-17 21:32:38 -04:00
Owner

The proxied names that reach `web` (`@`, `www`) cannot carry SSH, and the other DNS-only names point at `services`. So coaching-site's `refresh_demo` job (`DEMO_REFRESH_SSH_HOST`, a Forgejo secret) can only be a raw web IP today, which breaks when `web` moves to OVH.

  • Cloudflare module: `web-ssh` A and AAAA records to `web_ip` / `web_ipv6`, `proxied = false` (`web_unproxied_records`, plus two resources). deSEC module: the same name in its flat maps, so the later DNS move keeps it. README lists it.
  • Terraform applies it with the rest of the DNS module; `./setup --workspace ovh` points it at the new web at cutover.
  • `tofu fmt -check` and `tofu validate` pass on both modules. Nothing is applied by this PR.

deSEC and Cloudflare stay equivalent. web-ssh is in both modules. Checking the rest, the deSEC module still had a mail host that was removed from Cloudflare as unused (#90, 526); a DNS move would have brought it back. It is removed from deSEC, and scripts/tests/test_dns_parity.py (in lint) now compares the host names of both modules (and that SPF, both DKIM and DMARC exist in each), so this kind of drift fails CI. Verified the test fails when mail is put back. deSEC's one extra record, the Cloudflare-Email-Routing MX, has no Cloudflare-module twin by design (Email Routing is managed outside the module). Both modules pass tofu fmt and tofu validate; no plan or apply was run.

Trade-off: unlike the proxied names, it reveals web's origin IP. Check that web's firewall only accepts Cloudflare addresses on 80/443 before relying on the IP being unimportant (SSH is already reachable by IP).

After merging (operator)

  1. staging-validate green, `./infra approve`, `git pull`.
  2. `./infra deploy` (full promotion): its plan should show exactly 2 adds (the web-ssh A and AAAA records) and nothing else; stop and tell me if it shows anything else.
  3. `dig +short web-ssh.jfraeys.com A` returns web's IP (not a Cloudflare address). Then set the Forgejo secret `DEMO_REFRESH_SSH_HOST` of `coaching-site` to `web-ssh.jfraeys.com`; the next `refresh_demo` run uses it. Existing SSH host-key pins (known_hosts) for the old IP may need the new name accepted once.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ

The proxied names that reach \`web\` (\`@\`, \`www\`) cannot carry SSH, and the other DNS-only names point at \`services\`. So coaching-site's \`refresh_demo\` job (\`DEMO_REFRESH_SSH_HOST\`, a Forgejo secret) can only be a raw web IP today, which breaks when \`web\` moves to OVH. - Cloudflare module: \`web-ssh\` A and AAAA records to \`web_ip\` / \`web_ipv6\`, \`proxied = false\` (\`web_unproxied_records\`, plus two resources). deSEC module: the same name in its flat maps, so the later DNS move keeps it. README lists it. - Terraform applies it with the rest of the DNS module; \`./setup --workspace ovh\` points it at the new web at cutover. - \`tofu fmt -check\` and \`tofu validate\` pass on both modules. Nothing is applied by this PR. **deSEC and Cloudflare stay equivalent.** `web-ssh` is in both modules. Checking the rest, the deSEC module still had a `mail` host that was removed from Cloudflare as unused (#90, 526); a DNS move would have brought it back. It is removed from deSEC, and `scripts/tests/test_dns_parity.py` (in lint) now compares the host names of both modules (and that SPF, both DKIM and DMARC exist in each), so this kind of drift fails CI. Verified the test fails when `mail` is put back. deSEC's one extra record, the Cloudflare-Email-Routing MX, has no Cloudflare-module twin by design (Email Routing is managed outside the module). Both modules pass `tofu fmt` and `tofu validate`; no plan or apply was run. **Trade-off:** unlike the proxied names, it reveals web's origin IP. Check that web's firewall only accepts Cloudflare addresses on 80/443 before relying on the IP being unimportant (SSH is already reachable by IP). ## After merging (operator) 1. staging-validate green, \`./infra approve\`, \`git pull\`. 2. \`./infra deploy\` (full promotion): its plan should show exactly 2 adds (the web-ssh A and AAAA records) and nothing else; stop and tell me if it shows anything else. 3. \`dig +short web-ssh.jfraeys.com A\` returns web's IP (not a Cloudflare address). Then set the Forgejo secret \`DEMO_REFRESH_SSH_HOST\` of \`coaching-site\` to \`web-ssh.jfraeys.com\`; the next \`refresh_demo\` run uses it. Existing SSH host-key pins (known_hosts) for the old IP may need the new name accepted once. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
feat(dns): web-ssh, a DNS-only name for the web host
Some checks failed
Lint / lint (pull_request) Has been cancelled
Secret scan / scan (pull_request) Has been cancelled
55157357f5
Cloudflare's proxy cannot carry SSH and the proxied web names hide the node, so CI had only a raw IP
for web (coaching-site DEMO_REFRESH_SSH_HOST). setup re-applies DNS to the new web_ip at cutover,
so the name follows the node. Same record in the deSEC module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
fix(dns): deSEC drops the removed mail host; parity test for the two DNS modules
All checks were successful
Lint / lint (pull_request) Successful in 2m48s
Secret scan / scan (pull_request) Successful in 16s
e8fb943fdf
mail.jfraeys.com was removed from the Cloudflare module as unused (#90) but stayed in deSEC, so a DNS
move would have brought it back. test_dns_parity.py compares the host names of both modules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!275
No description provided.