Architecture¶
certify-reverse is a small Python bootstrapper around Caddy, xcaddy, Docker Compose, and dnsmasq. Its design favors explicit generated files and inspectable shell operations over a long-running management daemon.
Component view¶
repository configuration
├── .env
└── upstreams.yml
|
v
certify_reverse.cli
├── validates configuration
├── checks/builds Caddy DNS plugin
├── renders Caddyfile + dnsmasq.conf
├── exports internal CA material
├── writes dashboard + JSON snapshots
└── execs Caddy as PID 1
|
+--------------------------+
| |
v v
Caddy reverse proxy dnsmasq service
ports 80/443 port 53 TCP/UDP
|
v
configured upstream services
Source layout¶
| Path | Responsibility |
|---|---|
src/certify_reverse/cli.py |
Configuration model, runtime bootstrap, Caddy build, certificate exports, status data |
src/certify_reverse/templates.py |
Caddyfile, dnsmasq, and TLS guide rendering |
src/certify_reverse/status_page.py |
Self-contained browser dashboard |
src/certify_reverse/status_cli.py |
Terminal status view |
caddy-docker.sh |
Operator, release, and documentation commands |
docker/ |
Container image and Compose models |
tools/docs/ |
Pinned documentation publishing pipeline |
examples/ |
Runnable tutorial overlays and applications |
tests/ |
Unit, regression, shell, Compose, and publishing checks |
Runtime flow¶
- Docker runs
/usr/local/bin/boot. boot.shcreates writable runtime directories and invokescertify-reverse.- The application loads
/config/.envand/config/upstreams.yml. - Dataclass validation rejects malformed or unsafe values.
- The application checks whether
/data/caddy-rebuildexists, contains the required exact DNS provider module, and matches an explicit version pin. - When needed, xcaddy builds a temporary binary under
/data/caddybuild. - The temporary executable must run, report a semantic Caddy version, expose the exact requested DNS module, and match an explicit version pin.
- Only a validated and fsynced temporary binary atomically replaces the active binary.
- The application renders and atomically replaces runtime files.
- Optional trust extensions and certificate export/generation steps run.
- Caddy starts with the generated file or
Caddyfile.overwrite. os.execvpreplaces the Python process, making Caddy PID 1.
Configuration boundaries¶
The runtime has three configuration layers:
- image defaults;
- container process environment;
- mounted
/config/.env.
The mounted file is authoritative and overrides inherited image values. This avoids
a stale image CADDY_VERSION silently defeating the deployment configuration.
upstreams.yml has a deliberately narrow schema. Unknown fields fail instead of
being ignored.
Caddy build strategy¶
The active binary is persisted below /data rather than rebuilt into the image on
every configuration change.
requested provider/version
|
v
check existing binary
| exact plugin present?
| explicit version matches?
+-- yes -> reuse
+-- no -> xcaddy build temporary file
|
+-- executable?
+-- exact plugin present?
+-- requested version matches?
+-- all yes -> fsync + atomic replace
+-- any no -> keep previous binary
Build caches, module caches, temporary files, and HOME/XDG directories are placed
below writable runtime data so a non-root container does not attempt to use
/.cache, /.config, or /.local.
Generated Caddy model¶
The renderer creates:
- one site block per upstream;
- a status host with server-side probe routes;
- an internal-CA public certificate download host;
- a wildcard convenience host;
- a DNS-01 TLS block using runtime environment placeholders.
The DNS token is not embedded directly in the generated Caddyfile. Caddy receives it
through CADDY_DNS_PLUGIN_TOKEN.
IPv6 upstream literals are bracketed in both normal and probe routes.
Data and atomicity¶
Generated text, JSON, HTML, CA files, and static assets use same-directory temporary
files followed by os.replace and a best-effort parent-directory fsync. Caddyfile
formatting happens before that replacement rather than through a later in-place
rewrite. This prevents readers from seeing a partially written file after
interruption and improves durability after abrupt termination.
Service certificates use a paired staging process:
- Caddy writes temporary certificate and key files;
- both must exist and be non-empty;
- certificate mode becomes
0644; - private-key mode becomes
0600; - staged files replace active files.
The remaining full lifecycle—expiry-aware reuse, coordinated pair rotation,
consumer reload, and revocation behavior—is tracked in issues.yml.
Dashboard architecture¶
The status page is generated as one self-contained HTML document with embedded CSS and JavaScript. Runtime metadata and service definitions are serialized as inline JSON with script-closing characters escaped.
Browser data sources:
| Endpoint | Content |
|---|---|
/acme-state.json |
Generated ACME summary |
/crtsh-state.json |
Local crt.sh snapshot |
/probe/<service>/ |
Server-side upstream reachability proxy |
/probe/crtsh |
Server-side crt.sh proxy for live refresh |
The browser constructs rows with DOM APIs and textContent; runtime values are not
inserted with innerHTML.
Server-side GitHub and crt.sh snapshots are size-bounded before decoding. Malformed crt.sh rows are ignored, and only a bounded history set is persisted while the valid-row total remains available in metadata.
Certificate model¶
Browser-facing certificates¶
Caddy obtains these through ACME DNS-01. DNS provider credentials are needed for challenge records and renewal.
Internal upstream trust¶
When Caddy connects to an HTTPS upstream, it can:
- trust the exported internal CA;
- trust an explicit PEM file;
- temporarily skip verification.
The internal-CA host serves only public root CA files. Private keys are never routed through it.
dnsmasq model¶
The generated configuration provides one wildcard address rule:
It is intentionally simple. DHCP integration, split DNS, DNSSEC policy, IPv6 wildcard responses, and coexistence with another resolver remain deployment-level or deferred concerns.
Extension model¶
An HTTPS upstream may name a trust_ext.<ext_name> Python module. The runtime loads a
TrustExtension class, asks for status, and may issue or refresh trust material.
This interface is currently convention-based rather than a versioned plugin API.
The larger API and isolation work is tracked in issues.yml.
Packaging¶
Python uses a src/ package layout and setuptools. Console entrypoints are:
certify-reverse -> certify_reverse.cli:entrypoint
certify-reverse-status -> certify_reverse.status_cli:main
Wheel and source distribution builds are part of the release gate.
Documentation architecture¶
Canonical Markdown is rendered into several formats:
docs/*.md
├── MkDocs Material static site
├── Pandoc standalone handbook HTML
├── WeasyPrint A4 PDF
└── Pandoc roff man pages
Python documentation dependencies are hash-pinned. External renderer versions are
recorded and enforced, including Python and uv. A stale docs environment is
recreated when its Python version no longer matches. A manifest records source
order, tool versions, sizes, and SHA-256 hashes and is included in the deterministic
documentation archive.
Idempotency profile¶
Idempotent for fixed inputs:
- configuration validation;
- Caddyfile and dnsmasq rendering;
- static/dashboard generation;
- existing compatible binary reuse;
- documentation archive metadata when
SOURCE_DATE_EPOCHis fixed.
Intentionally or currently variable:
CADDY_VERSION=latest;- GitHub release lookup;
- crt.sh results;
- certificate authority timing;
- provider DNS propagation;
- generated certificate lifetimes;
- package-index contents when intentionally refreshing the docs lock.
Security boundaries¶
Sensitive:
- DNS provider token;
- Caddy private keys and certificate storage;
- generated service private keys;
- application secrets in tutorial/application environments;
- runtime backups.
Public or lower sensitivity by design:
- root CA certificates;
- generated Caddyfile placeholders without token values;
- static documentation artifacts;
- version and commit metadata.
The dashboard may still reveal operational topology and is unauthenticated by default. Protect it according to the deployment threat model.
Deferred architectural work¶
issues.yml tracks larger work not appropriate for a patch release, including:
- live DNS-provider integration coverage;
- dashboard authentication and metadata policy;
- a versioned trust-extension API;
- reproducible Caddy and container supply chain;
- dual-stack wildcard DNS;
- CI and protected release publishing;
- expiry-aware internal service certificate lifecycle;
- project license selection.