Skip to content

fix(vhosts): make SSL cert path validate and serve a real default cert#246

Merged
bihius merged 5 commits into
mainfrom
fix/letsencrypt-cert-path
Jun 14, 2026
Merged

fix(vhosts): make SSL cert path validate and serve a real default cert#246
bihius merged 5 commits into
mainfrom
fix/letsencrypt-cert-path

Conversation

@bihius

@bihius bihius commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Adding an SSL/Let's Encrypt vhost failed POST /config/apply with a "request failed" banner. The backend logged:

config-apply validation failed ... unable to stat SSL certificate from file
'/etc/haproxy/generated/certs/' : No such file or directory.

Root cause — two independent defects

Both reproduced and verified against a live stack.

  1. Fabricated default certificate. The fallback default.pem written by _write_candidate was hand-typed base64, not a real PEM. Even with the path fixed, haproxy -c rejects it with unable to load certificate ... too long.

  2. Unresolvable cert path during validation. The template bakes in the absolute path /etc/haproxy/generated/certs/, but:

    • the backend (where haproxy -c validation runs) mounts the shared generated_config volume at /var/lib/guard-proxy/generated, so that path does not exist there;
    • certificates were written per-release (<release>/certs/), not at the directory the template points to.

Fix

  • Generate a real self-signed default.pem via cryptography (already a dependency), written once and reused (idempotent). SNI selects the real certificate when present and falls back to this one otherwise — so an SSL vhost validates even before its Let's Encrypt certificate is provisioned.
  • Write certificates to a single shared <root>/certs directory (not per-release), matching the absolute path baked into haproxy.cfg.
  • Alias /etc/haproxy/generated to the backend mount point in the backend image so haproxy -c resolves the same shared volume HAProxy uses at runtime.

The shared generated_config volume and all mounts are unchanged — no change to mount points, RUNTIME_GENERATED_CONFIG_ROOT, or the HAProxy command/healthcheck.

Tests

  • ruff check app/, mypy app/, pytest --cov — all green (393 passed).
  • New regression tests: default cert is loadable as a real cert + key, generation is idempotent, and certs land in the shared directory with a fallback default.

Verification

End-to-end on a live backend container: symlink resolution + cryptography-generated default cert + haproxy -c on an SSL bind → passes. Full live deploy + real Let's Encrypt vhost test still pending (requires rebuilding the backend image on the target host).

bihius added 5 commits June 14, 2026 10:42
Adding an SSL/Let's Encrypt vhost failed config-apply with "request failed"
because the generated haproxy.cfg references an absolute cert directory that
could not be resolved, and the placeholder certificate was not a real PEM.

Two defects fixed:

- The fallback default.pem was fabricated base64 that `haproxy -c` rejected
  ("too long"). Replace it with a self-signed certificate generated via
  `cryptography`, written once and reused (idempotent). This lets an SSL vhost
  validate before its real certificate is provisioned; SNI selects the real
  cert when present and falls back to this one otherwise.

- Certificates were written per-release, but the template bakes in the
  absolute path /etc/haproxy/generated/certs. The backend mounts the shared
  volume at /var/lib/guard-proxy/generated, so that path did not exist during
  `haproxy -c` validation. Write certs to a single shared <root>/certs
  directory and alias /etc/haproxy/generated to the backend mount point so the
  same shared volume resolves identically during validation and at runtime.
  The shared volume layout and mounts are unchanged.

Add regression tests for default cert loadability/idempotency and the shared
certs directory layout.
@bihius bihius merged commit 5c7dd05 into main Jun 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant