Skip to content

feat(deploy): add dynamic DNS, a host firewall and read-only database access - #15

Merged
BryanLawless merged 2 commits into
devfrom
feat/production-access
Nov 4, 2026
Merged

BryanLawless merged 2 commits into
devfrom
feat/production-access

Conversation

@BryanLawless

@BryanLawless BryanLawless commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Dynamic DNS

  • favonia/cloudflare-ddns 1.18.0 (pinned by digest) keeps PUBLIC_DOMAIN (proxied), REGISTRY_DOMAIN and STORAGE_DOMAIN (DNS only, because of Cloudflare's 100 MB proxy body limit) on the connection's current public IPv4 address, checking on start and every five minutes. IPv6 is off.
  • Runs in the infrastructure-configs stage, so the records are right before the platform and apps deploy. That stage now takes cluster-settings through postBuild.substituteFrom (nothing in it used $ before), and validate.sh checks it with substitution.
  • Reuses the snapflow/cloudflare-dns Zone:DNS:Edit token cert-manager uses, and sends failed updates to the Flux alerts Discord channel; the ExternalSecret template turns the webhook URL into Shoutrrr's discord://<token>@<id> without any $, so Flux substitution cannot touch it.
  • Hardened like the chart's workloads: non-root, read-only root filesystem, all capabilities dropped, no service account token, secrets mounted as files, Reloader on secret changes.

Host firewall

  • deploy/bootstrap/host-firewall.sh is the server's whole UFW configuration, applied declaratively (reset, then the rules), so reruns are no-ops and hand-added rules are removed. --dry-run prints the rules.
  • Public: TCP 80, 443, 8443, 9443 and UDP 8443. Administrative network only (default: the default route's network; ADMIN_CIDR overrides; Tailscale when present): SSH, rate limited, and the k3s API. The k3s pod and service CIDRs are allowed for input and forwarding, per the k3s requirements for UFW.
  • Ends by listing every Docker-published port and Kubernetes NodePort, LoadBalancer, host-network and host-port workload, since those bypass UFW.

Read-only database access

  • CloudNativePG managed.roles declares snapflow_readonly (login, pg_read_all_data, connection limit 5) with its password from a kubernetes.io/basic-auth secret labelled cnpg.io/reload.
  • The OpenBao configure job generates the password at snapflow/platform/postgres-readonly; the operator policy may read that one path (and list snapflow/platform/) and nothing else there.

Testing

  • validate.sh passes for every stage, including infrastructure-configs with cluster settings; shellcheck is clean.
  • The notification template, rendered with Go text/template and sprig as External Secrets does, turns https://discord.com/api/webhooks/<id>/<token> into discord://<token>@<id>.
  • The pinned cloudflare-ddns image, run as 1000:1000 read-only with all capabilities dropped and these exact settings, parsed every setting (three IPv4 domains, @every 5m, notifications) and proceeded to call Cloudflare.
  • The firewall script in a privileged Ubuntu container: the dry run prints the rules; a real run produces exactly them; a second run is identical; a hand-added rule is removed; a bad argument is rejected.
  • Against OpenBao 2.7.1, the operator token reads snapflow/platform/postgres-readonly and lists snapflow/platform/, but cannot write it or read snapflow/platform/redis.

… access

Dynamic DNS: favonia/cloudflare-ddns keeps the public site, registry and
storage records on the home connection's current IPv4 address, in the
infrastructure-configs stage so it runs before the platform and apps. It
reuses the cert-manager DNS token and reports failures to the alerts
channel; that stage now takes the shared cluster settings.

Host firewall: deploy/bootstrap/host-firewall.sh applies the server's whole
UFW configuration declaratively: public edge and Traefik ports, SSH and the
k3s API from the administrative network only, and the k3s pod and service
networks. It lists every port Docker or Kubernetes exposes past UFW.

Database access: CloudNativePG manages a snapflow_readonly role in
pg_read_all_data, with a password generated in OpenBao that the operator
can read in the UI.

docs/reference/production-settings.md maps where every production value
lives.
@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 029d9ccd-42da-4d26-b3f2-15b8a4eb3335

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Nov 4, 2026
@BryanLawless
BryanLawless merged commit 830832e into dev Nov 4, 2026
15 checks passed
@BryanLawless
BryanLawless deleted the feat/production-access branch November 4, 2026 21:17
BryanLawless added a commit that referenced this pull request Nov 5, 2026
Pull requests are now squash-merged directly into main; dev, its workflow, sync-dev and the dev ruleset are retired. Pushes to main run CI, publish the changed images and run release-please, and Flux image automation pins digests on main.

Also brings in the changes merged into dev since the last promotion:
- feat(deploy): manage operator secrets in the OpenBao web UI (#14)
- feat(deploy): add dynamic DNS, a host firewall and read-only database access (#15)
- chore: remove the docs folder (#16)
- chore: remove dead code, configuration and unused runtime options (#17)
- fix(api): restore the executable bit on the GeoIP download script (#19)
- ci: rebuild the api image when the GeoIP download script changes (#20)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant