Skip to content

feat: support externally managed TLS via tls_external_cert_and_key option#860

Merged
hpk42 merged 6 commits intomainfrom
hpk/tls-external
Feb 24, 2026
Merged

feat: support externally managed TLS via tls_external_cert_and_key option#860
hpk42 merged 6 commits intomainfrom
hpk/tls-external

Conversation

@hpk42
Copy link
Contributor

@hpk42 hpk42 commented Feb 19, 2026

Add tls_external_cert_and_key config option for chatmail servers that manage their own TLS certificates outside of ACME. When set, the deployer verifies the cert and key files exist on the server and installs a systemd path unit that watches the certificate via inotify — when it changes, dovecot and nginx are automatically reloaded (postfix reads certs per handshake so needs no reload). Includes unit tests, an e2e test script, docs, and a CI workflow.

this is based on the prior work of self-signed TLS certs in #855

PR replaces and closes #662

@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 19, 2026 13:41 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 13:41 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 19, 2026 13:41 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 13:46 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 19, 2026 13:46 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 19, 2026 13:46 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 19, 2026 14:00 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 19, 2026 14:00 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 14:00 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 15:40 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 19, 2026 15:40 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 19, 2026 15:40 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 18:26 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 19, 2026 18:26 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 19, 2026 18:26 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 18:29 — with GitHub Actions Inactive
@hpk42 hpk42 had a problem deploying to staging2.testrun.org February 19, 2026 18:29 — with GitHub Actions Error
@hpk42 hpk42 had a problem deploying to staging-ipv4.testrun.org February 19, 2026 18:29 — with GitHub Actions Error
@hpk42 hpk42 had a problem deploying to staging-ipv4.testrun.org February 19, 2026 18:31 — with GitHub Actions Error
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 18:31 — with GitHub Actions Inactive
@hpk42 hpk42 had a problem deploying to staging2.testrun.org February 19, 2026 18:32 — with GitHub Actions Error
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 19, 2026 18:33 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 20, 2026 18:45 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 20, 2026 18:50 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 20, 2026 18:50 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 20, 2026 18:50 — with GitHub Actions Inactive
@hpk42 hpk42 had a problem deploying to staging-ipv4.testrun.org February 20, 2026 19:05 — with GitHub Actions Failure
@hpk42 hpk42 had a problem deploying to staging2.testrun.org February 20, 2026 19:07 — with GitHub Actions Failure
@hpk42 hpk42 temporarily deployed to staging.chatmail.at/doc/relay/ February 20, 2026 22:35 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 20, 2026 22:35 — with GitHub Actions Inactive
@hpk42 hpk42 temporarily deployed to staging-ipv4.testrun.org February 20, 2026 22:35 — with GitHub Actions Inactive
@hpk42 hpk42 had a problem deploying to staging-ipv4.testrun.org February 20, 2026 22:43 — with GitHub Actions Failure
@hpk42 hpk42 temporarily deployed to staging2.testrun.org February 20, 2026 22:48 — with GitHub Actions Inactive
)


def get_tls_deployer(config, mail_domain):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-level problem with this is that if you have a server with acmetool and want to reconfigure with external certificate, acmetool does not get uninstalled.

I think what we need is not selecting one deployer, but run all deployers with some option like enabled=... that tells the deployer if it should deploy or un-deploy acme or tls-cert-reload services.

Copy link
Contributor Author

@hpk42 hpk42 Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is addressed in the separate #869 (the first commit, the second does some follow up refactoring)

all other review comments were fixed in the branch here.

domain: staging-ipv4.testrun.org
secrets:
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't want to further debug the workflow. Thorough TLS-mode testing is better done whne we have a more flexible provisioning of VPS and DNS for PRs.

Comment on lines 369 to +389
@@ -381,10 +381,12 @@ def iter_output(self, logcmd=""):
while 1:
line = self.popen.stdout.readline()
res = line.decode().strip().lower()
if res:
yield res
else:
if not res:
break
if ready is not None:
ready()
ready = None
yield res
Copy link
Contributor Author

@hpk42 hpk42 Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a test fix on the side for a flaky test, took me a while to figure out. There are concurrency issues with getting log lines and triggering the send message. I think this is now reliable.

server via SCP, runs ``cmdeploy run``, and then probes all TLS-enabled
ports (nginx, postfix, dovecot) to verify the certificate is actually
served. After probing, checks remote service logs for errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test is only manually run, and partially machine generated.

Copy link
Contributor

@j4n j4n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing some more, it looks good, though there is one problem: the oneshot trigger reload fails on fresh deploys as services are not running yet.

)
# Trigger the oneshot service so services pick up the current cert.
# The path unit handles future changes via inotify.
systemd.service(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a fresh deploy (or Docker container start), those services aren't running yet, so the reload fails. We can probably just remove this as dovecot/nginx read the cert on startup, and the .path watcher handles live cert changes via inotify.

@j4n
Copy link
Contributor

j4n commented Feb 23, 2026

One caveat turned up: inotify doesn't cross bind-mount boundaries and if certificates are modified outside of the container, the reload must be triggered explicitly.

Copy link
Contributor

@j4n j4n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since its a simple remove of the system reload section, I guess I should still approve.

@hpk42
Copy link
Contributor Author

hpk42 commented Feb 23, 2026

@j4n i addressed your three comments in 3f05a9a

…tion

Adds a new tls_external_cert_and_key config option for chatmail servers
that manage their own TLS certificates (e.g. via an external ACME client
or a load balancer).

A systemd path unit (tls-cert-reload.path) watches the certificate file
via inotify and automatically reloads dovecot and nginx when it changes.
Postfix reads certs per TLS handshake so needs no reload.

Also extracts openssl_selfsigned_args() so cert generation parameters
are shared between SelfSignedTlsDeployer and the e2e test.
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.

3 participants