166 lines
9.2 KiB
Markdown
166 lines
9.2 KiB
Markdown
# Automatic certificate renewal
|
|
|
|
## Scope and defaults
|
|
|
|
RustProxy 0.7.0 renews **configured managed certificates** whose `issuer` metadata
|
|
identifies an existing HTTP-01, Cloudflare, OVH or RFC2136 Certbot profile. Issuance
|
|
and initial library import are still explicit, staging-first operations. Importing
|
|
a PEM pair alone does not create a managed lineage or authorize CA requests.
|
|
|
|
For managed certificates, `issuer.auto_renew` defaults to `true`, including saved
|
|
0.6 metadata lacking that field. Explicit `false` opts out. Therefore upgrading
|
|
an already configured managed certificate can enable scheduled CA checks; review
|
|
this before starting the new binary. Missing profiles fail closed with an error;
|
|
there is no silent recreation, provider switch or production issuance.
|
|
|
|
Certificates shows an independent enable/disable action, Check now and Dry run,
|
|
last check, next check, expiry and result. Expiry is populated after a worker check;
|
|
it is not a pre-existing inventory of every manually imported certificate. Renewal
|
|
is not a configurable new ACME account lifecycle or revocation feature.
|
|
|
|
## Schedule and recovery
|
|
|
|
Saved renewal observations are loaded from SQLite **before administration listeners are
|
|
exposed**, so last check, next check, expiry and result are available immediately after a
|
|
restart without pressing **Check now**. RustProxy then waits 5-10 minutes before the first
|
|
automatic CA check, avoiding a renewal burst during startup. Successful checks are scheduled
|
|
approximately every 12 hours with up to 30 minutes of additional jitter. Only one issue/renew
|
|
worker runs at once. Failures back off from one to six hours; a missing/invalid profile is
|
|
checked again after one hour. Observation rows contain non-secret status/timestamps only and
|
|
are stored only in `config.sqlite3`. RustProxy 0.15.0 no longer reads or imports historical JSON
|
|
observation sidecars at runtime. When SQLite has no observation row, the normal scheduler safely
|
|
reconstructs status on its next automatic check; this does not replace or regenerate certificate,
|
|
key, account or renewal-profile material. A manual dry run does not postpone a real due check. The
|
|
service must be running; this is not an independent timer that works while RustProxy is stopped.
|
|
|
|
The helper runs `certbot renew --cert-name rustproxy` in the original isolated
|
|
lineage, reusing saved authenticator/account/credential settings. It does not use
|
|
`--force-renewal`, change the CA or run directory hooks. Do not add shell hooks
|
|
to the private Certbot renewal configuration: hooks already saved by an operator
|
|
are still part of Certbot's trusted configuration. Certbot decides when the
|
|
certificate needs renewal. A successful exit can mean nothing was due: actual
|
|
certificate and key bytes are compared with the currently installed generation.
|
|
|
|
For changed material, the helper verifies validity times using OpenSSL and creates
|
|
private immutable, content-addressed staging files. Rust validates the certificate/
|
|
key match and requested domain coverage, then validates a complete new snapshot
|
|
including current vhost assignments. Only a successful revisioned SQLite write
|
|
publishes that snapshot. New TCP and QUIC TLS handshakes use the new certificate;
|
|
existing connections are not restarted. A simultaneous manual replacement/removal
|
|
or automatic-renewal disable wins over an older automatic worker result.
|
|
|
|
Worker failure, invalid PEM/SANs, expiry regression or configuration conflict leaves
|
|
the previously active generation intact. If Certbot succeeded but publication did
|
|
not, the next check compares files again and can retry the same staged generation.
|
|
Unchanged checks do not write a new configuration revision. Changed certificate publications
|
|
use the ordinary revisioned configuration path, but certificate inventory changes are not part
|
|
of cache compatibility and therefore do not discard otherwise compatible hot/cold objects.
|
|
|
|
Private profile/file checks, single-worker and per-profile locks, bounded subprocess
|
|
timeouts and process-group cancellation constrain work. Original certificate files
|
|
and Certbot archives are retained, not automatically deleted. Immutable renewal
|
|
staging is limited to 1024 keys; initial issuance staging remains limited to 128.
|
|
Plan monitored disk space and controlled offline cleanup after verified backups;
|
|
there is no automatic certificate garbage collector or external expiry alert yet.
|
|
|
|
## Provider-specific prerequisites
|
|
|
|
HTTP-01 still requires public TCP port 80 to reach an active plaintext RustProxy
|
|
listener for every name. The reserved challenge handler exposes only bounded tokens
|
|
for the current worker's domains, before application auth/ACL/cache. It is not a
|
|
public file browser. Wildcard certificates use DNS-01, not HTTP-01.
|
|
|
|
Cloudflare, OVH and RFC2136 need their original DNS permissions, credentials and
|
|
propagation settings. Secrets are stored in private files, not returned by the
|
|
renew endpoint or included in desired configuration. They are not encrypted at
|
|
rest. The helper, Certbot, plugins and OpenSSL must be usable by the service user.
|
|
Keep absolute profile paths stable; moving the Certbot directory requires explicit
|
|
migration of its stored paths. A system-wide timer should not also manage these
|
|
RustProxy-isolated profiles.
|
|
|
|
For a certificate intentionally owned by a separate system Certbot instead of this managed
|
|
renewal subsystem, 0.28.2 can reference the Certbot lineage directly. Configure a manual
|
|
certificate id with both absolute paths, for example
|
|
`/etc/letsencrypt/live/example.com/fullchain.pem` and
|
|
`/etc/letsencrypt/live/example.com/privkey.pem`. RustProxy resolves the `live/` symlinks again on
|
|
every targeted reload, so a renewal wrapper only needs to call:
|
|
|
|
```bash
|
|
curl --fail --show-error -X POST \
|
|
-H 'Authorization: Bearer TOKEN' \
|
|
-H 'X-RustProxy-Certificate-Key: certbot-main' \
|
|
-H 'Content-Type: application/json' \
|
|
--data '{}' \
|
|
https://proxy-admin.example/api/certificates/shared-cert/reload
|
|
```
|
|
|
|
The service identity must be able to traverse the Certbot directories and read the files. RustProxy
|
|
0.28.2 accepts named POSIX ACL read access and does not require an exact `0600`/`0640` mode.
|
|
External certificate/key files must not be group/world-writable, and private keys must not be
|
|
world-readable. Parent directories must be root- or RustProxy-owned and not group/world-writable
|
|
(apart from root-owned sticky ancestors such as `/tmp`). The referenced files are not copied into
|
|
RustProxy backups.
|
|
|
|
For a normal Certbot lineage, grant traversal without directory listing and read only the archive
|
|
files. A default ACL is useful, but a renewer may create a new private key with mode `0600` and
|
|
mask inherited ACL permissions. The reliable hook therefore reapplies the named read ACL to the
|
|
newly resolved generation before calling the reload API:
|
|
|
|
```bash
|
|
setfacl -m u:rustproxy:--x /etc/letsencrypt/ /etc/letsencrypt/live/ /etc/letsencrypt/archive/
|
|
setfacl -m u:rustproxy:--x /etc/letsencrypt/live/example.com/ /etc/letsencrypt/archive/example.com/
|
|
setfacl -m u:rustproxy:r-- /etc/letsencrypt/archive/example.com/fullchain*.pem \
|
|
/etc/letsencrypt/archive/example.com/privkey*.pem
|
|
setfacl -m d:u:rustproxy:r-- /etc/letsencrypt/archive/example.com/
|
|
|
|
# Run these two resolved-file grants after renewal, before the API reload.
|
|
setfacl -m u:rustproxy:r-- \
|
|
"$(readlink -f /etc/letsencrypt/live/example.com/fullchain.pem)" \
|
|
"$(readlink -f /etc/letsencrypt/live/example.com/privkey.pem)"
|
|
```
|
|
|
|
For external material below `/etc/ssl`, traversal of the protected root can be granted separately:
|
|
|
|
```bash
|
|
setfacl -m u:rustproxy:--x /etc/ssl/
|
|
```
|
|
|
|
If direct read access to the Certbot tree is undesirable, use the copy/deploy workflow instead:
|
|
|
|
```bash
|
|
/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert \
|
|
--cert /etc/letsencrypt/live/example/fullchain.pem \
|
|
--key /etc/letsencrypt/live/example/privkey.pem
|
|
```
|
|
|
|
That command resolves the Certbot `live/` symlinks locally, validates and stages a managed pair,
|
|
then publishes the existing id. Do not use deploy for an id configured with external absolute
|
|
paths, and do not point either external-manual workflow at a certificate carrying RustProxy
|
|
`issuer` metadata; one lineage must have one owner.
|
|
|
|
Staging certificates are not trusted by browsers. A Dry run checks renewal
|
|
validation but does not deploy the staging result. Only an explicit production
|
|
issuance creates a production profile; renewal never upgrades staging implicitly.
|
|
|
|
## API
|
|
|
|
All routes below require the existing local administrator session and CSRF checks.
|
|
|
|
- `GET /api/acme`: current job, provider names, nominal interval and renewal states.
|
|
- `POST /api/acme/renew`: `{"certificate_id":"site","dry_run":false}`; returns 202
|
|
and a job. Only identity/options, no API credential resubmission.
|
|
- Certificate opt-out is a revisioned config update setting
|
|
`tls.certificates[].issuer.auto_renew` to `false`.
|
|
|
|
`--dry-run` is **not** a claim of live CA validation in this release's test report:
|
|
packaging tests used a simulated Certbot. Run a real staging/production-lineage
|
|
validation on your server. Full runtime scheduler/publication acceptance is pending.
|
|
|
|
## Primary references
|
|
|
|
Certbot renewal behavior and flags:
|
|
https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates
|
|
https://eff-certbot.readthedocs.io/en/stable/man/certbot.html
|
|
|
|
Challenge types: https://letsencrypt.org/docs/challenge-types/
|