Conversation
When an accessory is configured with custom SSL certificates via
certificate_pem and private_key_pem in its proxy config, kamal
fails with "unable to load certificate" during kamal-proxy deploy.
The root cause is that Kamal::Cli::App::SslCertificates, which
handles uploading PEM files into the kamal-proxy container, is only
called during app boot (cli/app.rb), never during accessory boot
(cli/accessory.rb). The kamal-proxy deploy command references cert
paths that don't exist because nobody uploaded the files.
I hit this while configuring a proxied imgproxy accessory on a
separate subdomain with a Cloudflare Origin Certificate.
The fix has two parts:
1. Pass role_name: "accessories/#{name}" when initializing the
proxy configuration for accessories. Without this, role_name is
nil and cert paths collapse to tls/cert.pem, which would collide
between multiple accessories or with an app role that also has no
name. The accessories/ prefix keeps them in a separate namespace
from app roles (e.g. tls/web/ vs tls/accessories/monitoring/).
2. Add Kamal::Cli::Accessory::SslCertificates, mirroring the
existing Kamal::Cli::App::SslCertificates. It creates the target
directory and uploads cert/key PEM files before the container is
registered with kamal-proxy. This is called in the accessory boot
path, between the env upload and docker run.
Fixes basecamp#1769
There was a problem hiding this comment.
Pull request overview
This PR fixes accessory deployments that use custom SSL PEM secrets by ensuring accessory proxy TLS paths are properly scoped and by uploading the accessory certificate/key into the kamal-proxy apps-config before kamal-proxy deploy runs.
Changes:
- Scope accessory proxy
role_nametoaccessories/<name>so generated TLS paths don’t collide with app roles or other accessories. - Add an accessory SSL certificate uploader invoked during
kamal accessory bootto create the TLS directory and upload cert/key PEMs. - Add test fixture + tests covering path scoping and the boot flow for custom SSL accessories.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/fixtures/deploy_with_accessory_custom_ssl.yml | Adds a minimal config fixture for an accessory with custom SSL PEM secrets. |
| test/configuration/accessory_test.rb | Verifies accessory TLS paths include accessories/<name>/... scoping. |
| test/cli/accessory_test.rb | Exercises kamal accessory boot output to confirm cert directory creation/upload + proxy args. |
| lib/kamal/configuration/accessory.rb | Passes role_name: "accessories/#{name}" into proxy config for correct TLS path generation. |
| lib/kamal/commands/accessory/proxy.rb | Adds command helper to create the accessory TLS directory under proxy apps-config. |
| lib/kamal/commands/accessory.rb | Delegates name so CLI can reference accessory name in messages. |
| lib/kamal/cli/accessory/ssl_certificates.rb | New uploader that writes accessory cert/key PEMs before container registration with proxy. |
| lib/kamal/cli/accessory.rb | Calls the new accessory SSL uploader during boot. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| proxy_config: accessory_config["proxy"], | ||
| role_name: "accessories/#{name}", | ||
| context: "accessories/#{name}/proxy", | ||
| secrets: config.secrets |
There was a problem hiding this comment.
role_name is built from the accessory name and then used as part of filesystem paths under the proxy TLS directory. Accessory names come directly from YAML keys and don’t appear to be validated for path separators or .. (contrast with alias name validation in lib/kamal/configuration/validator/alias.rb). Consider validating/normalizing accessory names before using them in paths to avoid unexpected directory layouts or path traversal when creating/uploading TLS files.
There was a problem hiding this comment.
The accessory name already ends up in filesystem paths in a bunch of
other places — not just the TLS dir this PR adds. The env file path at
configuration/accessory.rb:73 interpolates it directly, and service_name
(configuration/accessory.rb:27) becomes the docker container name and
is joined into host mount paths at configuration/accessory.rb:217. So
this PR isn't expanding the surface, just using the same convention.
Validating names feels like a separate change to me. A strict regex like
the alias one would also break anyone whose accessory names have
uppercase letters or dots. And since names come from the operator's own
deploy.yml, anyone who can write path separators in there can already do
whatever they want on the servers. Docker also rejects / in container
names, so most weird values blow up early anyway.
Raise an explicit error naming the missing secret and accessory instead of silently skipping the upload, which previously surfaced as a confusing "unable to load certificate" failure from kamal-proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Superseded by #1951: a rewrite from scratch on top of current main, reduced to the minimum that mirrors the app's certificate upload, with the certificate layout explained and CI run on my fork. Closing this one to keep the discussion in one place. |
This is my first contribution, please be extra careful with the review :) Sadly I didn't manage to run the integration tests on OSX. Original commit follows:
When an accessory is configured with custom SSL certificates via certificate_pem and private_key_pem in its proxy config, kamal fails with "unable to load certificate" during kamal-proxy deploy.
The root cause is that Kamal::Cli::App::SslCertificates, which handles uploading PEM files into the kamal-proxy container, is only called during app boot (cli/app.rb), never during accessory boot (cli/accessory.rb). The kamal-proxy deploy command references cert paths that don't exist because nobody uploaded the files.
I hit this while configuring a proxied imgproxy accessory on a separate subdomain with a Cloudflare Origin Certificate.
The fix has two parts:
Pass role_name: "accessories/#{name}" when initializing the proxy configuration for accessories. Without this, role_name is nil and cert paths collapse to tls/cert.pem, which would collide between multiple accessories or with an app role that also has no name. The accessories/ prefix keeps them in a separate namespace from app roles (e.g. tls/web/ vs tls/accessories/monitoring/).
Add Kamal::Cli::Accessory::SslCertificates, mirroring the existing Kamal::Cli::App::SslCertificates. It creates the target directory and uploads cert/key PEM files before the container is registered with kamal-proxy. This is called in the accessory boot path, between the env upload and docker run.
Fixes #1769