Supplied-QWAC Caddy integration
See the bring-your-own credential workflow for local key/CSR preparation, shared validation, import and safe configuration/reload replacement.
This plugin supports single-host, ACME-managed TLS with supplied RSA 2-QWAC credentials. The core signs and independently verifies a binding, durably commits it, and only then lets the ACME issuer return a replacement TLS certificate. It does not enroll QWACs, establish browser trust, check EU Trusted List status, fetch authority material, or claim full production readiness/legal qualification.
Installation
Section titled “Installation”Use packaged installation for Linux amd64 without Go or xcaddy. The quickstart includes supported Ubuntu setup. Use source installation for custom builds.
The packaged qwac-version --json command reports distribution version, source,
Caddy/core identities, platform and toolchain. list-modules and
go version -m remain useful advanced build diagnostics. Expect
tls.issuance.qwac and http.handlers.qwac, no qwac_demo and no development
module replacements. Public release downloads/tags remain unavailable;
authenticated candidates are not public releases.
State and verification
Section titled “State and verification”directory may be omitted: the conventional state is
<Caddy application data>/qwacamole/<explicit hostname>. Ubuntu setup sets
XDG_DATA_HOME=/var/lib/caddy-qwac; an explicit directory override is retained.
Choose one literal hostname and one exclusive state owner. Ambiguous/fallback
TLS policies are rejected by the existing policy validator.
qwac-check --hostname HOST --authority AUTHORITY.pem verifies normal HTTPS,
stable discovery, signer authority and current TLS coverage. Add --json for
machine diagnostics. Run on the owning host: it also reads the committed TLS set
from the packaged service’s conventional state directory. For manual deployments,
pass --state /your/binding/directory. A publication changing during verification
can report a generation mismatch; retry after renewal settles.
Add --firefox-compatibility only for a deployment explicitly
configured with the bare firefox_compatibility switch. It does not establish
native browser acceptance or legal qualification.
qwac-setup supports Ubuntu 24.04 amd64, takes the meaningful inputs shown in the
quickstart and optionally --acme-server https://.../directory. It validates the
actual Caddy configuration in isolated temporary state before installing files.
It never starts/stops existing services or overwrites changed configuration.
Use an identical repeat for a read-only check; use explicit maintenance for
credential replacement or edited files. The generated service runs as root,
with private input/state ownership and a restrictive umask. Other platform or
service arrangements remain manual and require independent evaluation.
For an update, build a separate binary at the selected immutable/module version,
inspect list-modules/go version -m, and validate config before replacement.
Preserve the previous binary and complete Caddy TLS storage, binding state,
credentials and configuration. Follow operations for full-stop
policy/state migrations and offline retirement. Start with the same owned paths
and confirm a protected request and binding/TLS identity. Roll back only to a
compatible binary and consistent complete state; expired prior inputs are not a
usable rollback target. Do not delete retained coverage to force an upgrade, or
use an unpinned automatic module upgrade as release validation.
Configuration
Section titled “Configuration”The small site-level qwac directive sets up both middleware and exactly one
QWAC ACME issuer. Shared settings are written once. It runs before redirection,
respond/proxy/file handlers, and handle/route blocks so /bindings/* stays
reachable. Avoid adding another tls issuer, global fallback issuer, on-demand
TLS, external certificate manager, manual TLS leaf, wildcard/IP host or a second
hostname to a protected site. Keep one literal lower-case DNS hostname per
site/state. Internationalized names must use their ASCII punycode spelling.
Ordinary unprotected sites may use their own Caddy TLS and application settings.
Complete examples:
- Reverse proxy.
- File server.
- Local Pebble smoke.
- Native JSON (replace
{STATE}with an absolute directory; it is a documentation placeholder, not a Caddy runtime variable).
Example site:
qwac.example.com { qwac { hostname qwac.example.com directory /var/lib/caddy/qwac chain_files /etc/caddy/qwac-chain.pem key_file /etc/caddy/qwac-key.pem trust_files /etc/caddy/qwac-authority.pem lifetime 1h acme { dir https://acme-v02.api.letsencrypt.org/directory } } reverse_proxy 127.0.0.1:8080}Use your real hostname/owner credentials and ACME reachability; the example name
is for local testing. Examples accept optional QWAC_SITE, QWAC_HOST,
QWAC_STATE, QWAC_CHAIN, QWAC_KEY, QWAC_TRUST, ACME_DIRECTORY, ACME_EMAIL,
UPSTREAM, and DOCUMENT_ROOT environment overrides, with readable defaults in
those files. For Pebble, STATE is an absolute test directory prepared by the
smoke runner, and no public CA is contacted.
The nested acme block delegates to Caddy’s pinned ACME issuer parser; ordinary
options such as directory/test directory, email, EAB, DNS challenge provider,
resolvers, timeouts, alternate challenge ports, profiles and CA roots remain
available. DNS providers require their usual additional Caddy modules. ACME
revocation and renewal-information interfaces remain supported. Any ACME retry
inside that wrapped issuer still passes the same binding gate.
Advanced explicit form: tls { issuer qwac { <same settings and acme block> } }
paired with qwac_binding { <identical binding settings, no acme block> }.
This is useful for existing layouts, but doubles the settings; validation rejects
inconsistent pairs. In manual route ordering, put qwac_binding first and
ensure it catches binding-document paths. Prefer the site-level directive.
Caddy’s extension reference
describes adaptation and directive ordering.
Credentials and state
Section titled “Credentials and state”chain_files is a leaf-first chain, in one PEM bundle or separate PEM files.
The last certificate is the configured QWAC authority; it may be omitted from
the bundle because the explicit trust_files authority is appended. Supply
exactly one authority certificate/file in trust_files. The plugin checks the
RSA key match, chain order/signatures/validity, supported 2-QWAC profile, SAN for
the configured hostname and chain verification against that explicit authority
before attempting issuance. The authority is for local consistency only, separate
from ordinary TLS/ACME roots and browser trusted/QWAC authorities. No root is
trusted because it happened to arrive in a leaf bundle. No AIA network fetching,
CA/key generation or browser trust changes happen in provisioning.
key_file supports an unencrypted RSA PKCS#1 or PKCS#8 PEM key. RS256 is the
only signing algorithm. Encrypted keys, HSM integration, filesystem watching and
automatic enrollment are not supported. The supported certificate profile is
QNCP-w-gen, only tls-binding EKU, digitalSignature, legal-person identity,
QC compliance/web-type statements, SAN, AIA and CRL metadata. Presence of those
fields does not establish revocation freshness or legal qualification.
The binding lifetime defaults to 1h, minimum 3s. It is capped by the
earliest QWAC-chain expiry and the latest retained TLS leaf expiry. Older TLS
leaves stay covered for authenticated held connections alongside renewed leaves.
Refresh uses actual signed expiry,
with a default margin of one third of the configured/effective lifetime. JSON
accepts readable durations or legacy numeric nanoseconds. See the
operational policy for limits, diagnostics and offline retirement.
Strict standard bindings are default. Set the explicit
firefox_compatibility switch (JSON true) for the known Firefox crit-header
compatibility variant; it is never chosen from User-Agent.
Use absolute credential/state paths in deployment. Caddy’s OS account must read
the credentials and exclusively write state; provision state directories mode
0700 and key files mode 0600, with backups/access restricted accordingly. Newly
created state/document/manifest files use those modes. Existing directory
permissions are the operator’s responsibility. The live manager holds an OS
file lock; another process cannot provision the same state. Do not remove its
lock file while Caddy runs. Lock ownership ends at final configuration cleanup
or process exit; an existing unlocked lock file is harmless. A separate
caddy validate process cannot open state owned by a running server; validate
before startup, or use the live admin reload for authoritative validation.
Reload and credential replacement
Section titled “Reload and credential replacement”./caddy adapt --config Caddyfile --adapter caddyfile --pretty > caddy.json./caddy validate --config caddy.json # before starting the state owner./caddy run --config Caddyfile --adapter caddyfile# Later, after replacing the supplied leaf/key files:./caddy reload --config Caddyfile --adapter caddyfile --forceExplicit --force reload is necessary when configuration bytes remain unchanged
and only files change. Replace the leaf/bundle and its matching key consistently;
prefer atomic file replacement before reload, avoid in-place truncation of live
files. Inconsistent files reject provisioning without installing the candidate.
Each new configuration rereads and validates the supplied files. An overlapping
config stages credentials and all retained/stored/new TLS leaves together; at
first live use one complete replacement binding is signed, independently verified
and durably published before switching signer, coverage or generation. Old handlers
and issuance tasks continue sharing current material/state and cannot reinstall
an older credential generation. Only one refresh worker exists per state owner;
old cleanup cannot stop a new lease. Rejected validation/reload starts no new
worker and retains no unused lease. Publication failure keeps the prior binding
and credentials active and protected requests fail closed if it expires.
Only leaf/key replacement under the same configured authority is supported. Authority migration requires a separately designed operational/browser trust transition and is unsupported. Hostname, profile, lifetime/state ownership changes are rejected while overlapping with a conflicting live owner; plan a stop/reconfigure rather than mutating shared state settings during reload. Restart validates/reuses the atomic manifest and immutable documents, reconstructs retained coverage, and republishes before advertising recovered material.
HTTP behavior and failure handling
Section titled “HTTP behavior and failure handling”The advertised discovery endpoint is /bindings/current.jose, stable across
refresh, renewal and QWAC replacement. GET returns a verified copy of one committed
current document, application/jose, Cache-Control: no-store. Expired/unavailable
current state returns 503. Binding responses never advertise themselves.
Immutable /bindings/<64 lower-case hex SHA-256>.jose artifacts remain separate
for recovery and grace-protected historical access. GET returns independently
verified, content-addressed, retained application/jose,
Cache-Control: no-store. HEAD has identical response metadata without a body;
other methods return 405 with Allow: GET, HEAD. Invalid names, traversal,
symlinks, unknown files or modified/unverifiable documents return 404. Historical
immutable documents remain available until collection eligibility (including
expired documents, whose signed expiry the consumer must evaluate).
The endpoint never maps arbitrary files/private material.
For ordinary protected responses the middleware appends the stable discovery Link at final response commit, after downstream headers. Other application Link values and Cache-Control survive. Status, bodies and streaming are passed through without buffering. Applications should reserve the binding relation for this middleware. An unavailable/expired binding produces 503 instead of running the protected application; unrelated hostnames pass through. A new TLS certificate cannot be returned by the issuer until signing, verification and durable publication all succeed. Refresh/publication/recovery errors use Caddy structured logs. Existing valid TLS/bindings remain usable during recoverable failures and automatic ACME retry/independent refresh can recover; inspect Caddy logs before changing trust.
Every published TLS leaf remains in binding coverage, including an authenticated HTTP/2 connection that outlives leaf expiry. This does not waive expiry checks on new handshakes. Automatic document collection preserves current/staged artifacts and a default 24h grace after both signed expiry and file modification. It never prunes TLS coverage. Enforced defaults refuse additions beyond 256 retained leaves or publications above 1 MiB; exceeding capacity can prevent TLS renewal. Follow the full drain/offline retirement procedure before capacity is reached. Distributed storage is unsupported; multiple independent single-host owners may run in one Caddy instance with separate state.
Caddy evaluates handle_errors outside the normal middleware chain. Put a
qwac_binding handler with identical settings first inside custom error routes
that must advertise bindings. This preserves arbitrary error-handler Link/cache
headers at commit; the site-level normal handler cannot wrap that separate chain.
In advanced JSON, pair the same middleware in servers.*.errors.routes.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Check and recovery |
|---|---|
| Private module cannot be fetched | Confirm repository access with your normal Git credentials, scoped GOPRIVATE, and the immutable ref above. The repository is private. |
| Caddy JSON module decoding fails | Use the documented Caddy/Go baseline and explicit GOEXPERIMENT=jsonv2; older Caddy releases have an incompatible RawMessage lookup. |
| Credentials fail before issuance | Check leaf-first PEM order, unencrypted RSA key match, validity, hostname SAN/profile, and the separately supplied authority. Inspect the complete provisioning error; TLS roots are not QWAC authorities. |
| State is already owned | Stop the other owner, or use a separate state directory. A standalone validation process also conflicts with a running owner. Do not delete its lock file. |
| Reload does not pick up new files | Use caddy reload --force; unchanged configuration bytes otherwise need not reprovision. Check the reload exit status and the activated signer in structured logs. |
| Reload is rejected | Repair the supplied files or inconsistent settings and retry. The previous live configuration remains active; no candidate worker is started by validation. |
| Protected response is 503 | Inspect refresh/publication logs, credential validity, state permissions and disk space. Restore signing/publication so independent refresh or gated issuance can recover. Do not bypass the gate or erase retained history. |
| HTTPS works but the browser shows no qualified identity | Local plugin validation is separate from the browser’s QWAC authority list, feature support and binding profile. Use the isolated demonstration and browser compatibility guide; a page label is insufficient. |
Evaluate before deployment
Section titled “Evaluate before deployment”Use the isolated demonstration to see native identity and binding refresh with synthetic credentials. Check support and browser compatibility before interpreting native identity indicators. Original project code is MIT licensed.