Files
computer-playbook/group_vars/all/02_tls.yml
kevinveenbirkenbachandClaude Opus 5 b5fcd535de fix(ca): extend the container trust store instead of replacing it
SSL_CERT_FILE, CURL_CA_BUNDLE and REQUESTS_CA_BUNDLE replace the trust store;
only NODE_EXTRA_CA_CERTS extends it. compose_ca.py pointed all four at the
deployment own root CA, so every OpenSSL and rustls client in every container
was blind to every public certificate. Measured here, one variable, same URL:

    unset                          curl https://registry.npmjs.org/ -> 200
    a single unrelated CA          -> 000 unable to get local issuer certificate
    that CA + the system bundle    -> 200

PeerTube was the one process that hit it: its plugin install fetches from
registry.npmjs.org at runtime through a client that honours SSL_CERT_FILE, and
it failed identically in compose and in swarm with

    invalid peer certificate: UnknownIssuer

Everything else reaches its artefacts at image build time, or through Node,
whose NODE_EXTRA_CA_CERTS is additive and therefore kept working.

The replacement was never a posture. e7e8cf8c03 calls these variables an
"env-based CA trust fallback", f1ea036bd2 adds them "instead of falling back to
a plain docker run without CA trust", e79183f96a wires the extra CA in "so npm
traffic through the cache validates". Every commit framed them as adding trust.

sys-ca-selfsigned now builds ca-bundle.crt beside the root CA on every node,
from that node own files: the stack host mints the CA and workers fetch it, so
there is no cross-node dependency. The bundle is the root CA concatenated with
the first present CA_BUNDLE_CANDIDATES entry, the SPOT fc0881ff33 introduced.
The cat argument list is assembled in Jinja, so a host with no public bundle
emits the root CA alone, which is exactly the previous behaviour, rather than an
empty argument that would fail. bundle_host joins the existing stray-directory
sweep and the build runs after it.

compose_ca.py uses the bundle only when the file exists and is non-empty, so a
host that has not built one keeps the previous behaviour instead of binding a
missing path that Docker would materialise as a directory. The php ini, msmtprc
and the Matrix flavour may name the container path with no fallback because the
bundle is written at task 99 and the php ini at task 123: no host can carry the
ini without the bundle.

CA_TRUST_CERT and NODE_EXTRA_CA_CERTS stay on the single certificate. keytool,
occ security:certificates:import and update-ca-certificates each take one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:32:44 +02:00

48 lines
2.8 KiB
YAML

---
TLS_ENABLED: true # nocheck: unused-var # consumed by plugins/lookup/{tls,email,cert}.py
TLS_MODE: "letsencrypt" # Valid values letsencrypt, self_signed, off
TLS_SELFSIGNED_BASE_PATH: "/etc/{{ SOFTWARE_DOMAIN }}/selfsigned"
TLS_SELFSIGNED_SCOPE: "global" # nocheck: unused-var # consumed by the self-signed cert generator outside the YAML/Jinja scope
CERTBOT_ACME_CHALLENGE_METHOD: "cloudflare" # webroot, cloudflare, or hetzner
CERTBOT_CREDENTIALS_DIR: /etc/certbot
CERTBOT_CREDENTIALS_FILE: "{{ CERTBOT_CREDENTIALS_DIR }}/{{ CERTBOT_ACME_CHALLENGE_METHOD }}.ini"
CERTBOT_DNS_PROPAGATION_WAIT_SECONDS: 300 # How long should the script wait for DNS propagation before continuing
CERTBOT_FLAVOR: san # Possible options: san (recommended, with a dns flavor like cloudflare, or hetzner), wildcard(doesn't function with www redirect), dedicated
LETSENCRYPT_WEBROOT_PATH: "/var/lib/letsencrypt/" # Path where Certbot stores challenge webroot files
LETSENCRYPT_BASE_PATH: "/etc/letsencrypt/" # Base directory containing Certbot configuration, account data, and archives
LETSENCRYPT_LIVE_PATH: "{{ LETSENCRYPT_BASE_PATH }}live/" # Symlink directory for the current active certificate and private key
CA_ROOT:
base_dir: "/etc/{{ SOFTWARE_DOMAIN }}/ca"
cert_host: "{{ SOFTWARE_DOMAIN | ca_cert_host }}"
key_host: "/etc/{{ SOFTWARE_DOMAIN }}/ca/root-ca.key"
openssl_conf_host: "/etc/{{ SOFTWARE_DOMAIN }}/ca/openssl-root-ca.cnf"
days: 3650
key_bits: 4096
force_regen: false # When true, delete key+cert and regenerate
subject:
C: "DE"
O: "{{ DOMAIN_PRIMARY }}"
OU: "Infinito.Nexus Root CA"
CN: "{{ (DOMAIN_PRIMARY ~ ' Root CA') if ((DOMAIN_PRIMARY ~ ' Root CA') | length <= 64) else DOMAIN_PRIMARY }}"
CA_TRUST:
cert_host: "{{ CA_ROOT.cert_host }}"
bundle_host: "{{ [CA_ROOT.base_dir, 'ca-bundle.crt'] | path_join }}"
inject_bundle_container: "/tmp/infinito/ca/ca-bundle.crt"
cert_container: "/etc/ssl/certs/infinito-ca.pem"
trust_name: "{{ DOMAIN_PRIMARY | lower }}"
wrapper_host: "{{ BIN_CA_TRUST }}"
inject_script: "{{ BIN_CA_INJECT }}"
inject_cert_container: "/tmp/infinito/ca/root-ca.crt"
inject_wrapper_container: "/tmp/infinito/bin/with-ca-trust.sh"
php_ini_host: "{{ BIN_CA_PHP_INI }}"
inject_php_ini_container: "/usr/local/etc/php/conf.d/zz-infinito-ca-trust.ini"
CA_TRUST_SYSTEMD_ENV:
- "CA_TRUST_CERT_HOST={{ CA_TRUST.cert_host }}"
- "CA_TRUST_WRAPPER_HOST={{ CA_TRUST.wrapper_host }}"
- "CA_TRUST_NAME={{ CA_TRUST.trust_name }}"
- "CA_TRUST_CERT_CONTAINER={{ CA_TRUST.inject_cert_container }}"
- "CA_TRUST_WRAPPER_CONTAINER={{ CA_TRUST.inject_wrapper_container }}"
CA_TRUST_SYSTEMD_ENV_SELFSIGNED: >-
{{ CA_TRUST_SYSTEMD_ENV if (TLS_MODE | lower) == 'self_signed' else [] }}
CA_CONTAINER_ENABLED: true # When falsy the container wrapper skips CA injection on `container run`