Commit Graph
3 Commits
Author SHA1 Message Date
bitdeals 0801b0d376 feat: put the log in the volume, so the container can be read-only
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 1m4s
/var/log/letsencrypt was the only thing this image wrote outside its volumes,
and it is what stopped `read_only: true` from working. --logs-dir moves it to
/etc/letsencrypt/logs, beside the account key and the renewal config it
already keeps there.

The second gain is the one that matters more here. This is the service whose
breakage does not announce itself: a renewal that stops working is an expired
certificate sixty days later, and the log is what says why. On the root
filesystem it died with every container the registry replaced; in the volume
it outlives them.

Nothing else about the container needs to change to be confined. It never
changes user, the files it touches are its own, the ACME challenge is served
on 380 rather than a privileged port, and the HAProxy runtime socket it writes
to is group-owned by root -- so it reaches it by permission rather than by
CAP_DAC_OVERRIDE, and `cap_drop: ALL` takes nothing away. Measured on the live
relay: no file under /etc/letsencrypt or /etc/certificates is owned by anyone
but root, and the socket is srw-rw---- 1001:0.

Both READMEs say what a caller should set now, stop_grace_period included --
the entrypoint has asked for that one since it was written.
2026-09-10 11:49:04 +00:00
bitdeals a842e0771e fix: HAProxy could not read the placeholder, so a failed issuance took the site down
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 36s
The placeholder is the whole reason a first start works at all: HAProxy
resolves `bind ... ssl crt` while parsing its configuration, so it cannot
start without site.pem, and certbot writes a self-signed one to break
that circle. It wrote it 0600 root-only, under the umask the subshell
sets for the private key it builds it from. HAProxy runs unprivileged
(uid 1001 in the bitnami image), so it could not open the file and exited
-- and because it never came up, it never created the runtime API socket
the pass waits for, and the pass hung in that wait instead of reaching
the 12-hour sleep. A node whose first issuance failed -- a typo in
CERTBOT_DOMAIN is enough -- served nothing at all on 80 or 443, retried
nothing, and said so only in two container logs.

The real certificate escapes this by accident: 2-concatenate-cert.sh
writes it with `cat >` under the default umask, so site.pem is 0644 on
every node that ever issued one. That is why nobody hit this -- it needs
a first issuance that fails.

chmod on the temporary file rather than after the rename, so the atomic
rename stays the only thing HAProxy can observe and site.pem never exists
with a mode that stops it. The intermediates keep the tight umask.

Reproduced on a disposable haproxy+certbot stack with empty volumes and
CERTBOT_DOMAIN=testnet2.bitdeals.invalid: before, HAProxy crash-looped on
"cannot open the file" while certbot waited for admin.sock; after, HAProxy
starts on the placeholder, certbot reports the rejected domain, the loop
retries in 12h, and the site answers on 80 and terminates TLS on 443 with
the self-signed certificate.

Claude-Session: https://claude.ai/code/session_01BvgYcYPWd1KGABLKViVPSk
2026-08-24 13:13:23 +00:00
bitdeals e8204bbb4d fix: the certificate follows CERTBOT_DOMAIN, on every pass
A pass branched on whether /etc/letsencrypt/live/<first-name> existed:
present, it ran `certbot renew`; absent, it handed over to the creation
script. Only the second path was ever told the names to ask for, so
after the first issuance CERTBOT_DOMAIN stopped reaching certbot
altogether -- renew takes the names off the certificate it already holds.
Editing the variable did nothing, silently, and the README documented the
manual issuance needed to work around it.

The branch is gone. Every pass now runs certonly with the names spelled
out, and certbot decides what that means:

  same names, not due    -> "Certificate not yet due for renewal; no
                            action taken", no connection opened, nothing
                            spent against the rate limits
  name added or removed  -> reissued with exactly the requested set
  due for renewal        -> renewed

--keep-until-expiring is what makes the first line true, and --cert-name
(already passed) is what keeps a changed list updating the existing
lineage instead of starting a second one beside it. No --expand is
needed to add a name once the lineage is named.

All three decisions were checked against the live stands: the no-op one
with a real run on testnet2, the other two as dry runs on testnet1 and
testnet2.

With the branch removed, 1-renew-cert.sh is a pass-through and the
numbering finally matches the order things run in -- 0-create-cert.sh
used to execute after 1-renew-cert.sh. So: 1-renew-cert.sh deleted,
0-create-cert.sh renamed to 1-ensure-cert.sh, which is what it now does.

Claude-Session: https://claude.ai/code/session_01BvgYcYPWd1KGABLKViVPSk
2026-08-24 13:13:03 +00:00