Linux service targets¶
The Agent can deploy a renewed certificate to HAProxy, Caddy, Traefik, LiteSpeed, Postfix, Dovecot, PostgreSQL, MySQL and Tomcat on Linux, in addition to the NGINX, Apache httpd and Tomcat types of the terminal interface.
Tested on Debian 13
The starter hooks were run before release as root on Debian 13 against Apache httpd 2.4, HAProxy 3.0, Caddy 2.6, Traefik 3.7, OpenLiteSpeed 1.9, Postfix 3.10, Dovecot 2.4, PostgreSQL 17 and MariaDB 11.8 (Debian's MySQL), with certificates from Let's Encrypt's staging CA: each service served the issued certificate and then the renewed one. Other distributions and versions, and Oracle MySQL, were not tested. Read the generated script before the first renewal.
How it works¶
The Agent service runs unprivileged: it cannot write into /etc/haproxy or
reload a service itself. For these targets it uses the
root hook runner:
- The Agent saves the certificate as PEM files with an unencrypted key into its own folder (one the service may write; the terminal interface prepares it).
- A
hook.runstep asks the root hook runner to run the starter hook/etc/certintel/hooks.d/certintel-<kind>.sh(owned byroot:root, mode0700). - The hook installs the files where the service reads them, with the service's owner, group and mode, runs the configuration test and reloads the service.
The hook is an ordinary shell script; the paths, owner, group, mode and
commands are written into it and you can edit them. The configuration test
runs before the reload, so a mistake stops the script and the running service
keeps its current certificate. An existing hook is
never replaced: to start again, remove it first. Its output is in
journalctl -t certintel-agent-hooks.
A reload returns before the service serves the new certificate, so the starter hooks of services whose workflow then checks the endpoint wait briefly after the reload: 2 seconds for NGINX, Apache httpd, HAProxy and Caddy, 5 seconds for LiteSpeed (a full graceful restart), Traefik (it reads the files again once the hook touches its configuration) and Tomcat (after its restart).
Targets¶
$ID stands for the certificate ID ($CERTINTEL_CERTIFICATE_ID). The paths
are the defaults the terminal interface fills in; change them to what your
service reads.
| Kind | Installed files (default) | Owner:group | Key / certificate mode | Configuration test | Reload |
|---|---|---|---|---|---|
haproxy |
/etc/haproxy/certs/$ID.pem (one combined file: full chain and key) |
root:haproxy |
0640 |
haproxy -c -f /etc/haproxy/haproxy.cfg |
systemctl reload haproxy |
caddy |
/etc/caddy/certs/$ID.crt, $ID.key |
caddy:caddy |
0600 / 0644 |
caddy validate --config /etc/caddy/Caddyfile |
systemctl reload caddy |
traefik |
/etc/traefik/certs/$ID.crt, $ID.key |
root:traefik |
0640 / 0640 |
the file provider's configuration /etc/traefik/dynamic exists |
touch /etc/traefik/dynamic: Traefik watches its configuration, not the certificate files |
litespeed |
/usr/local/lsws/conf/$ID.crt, $ID.key |
lsadm:lsadm |
0600 / 0600 |
none: openlitespeed -t exits 1 on every configuration |
/usr/local/lsws/bin/lswsctrl restart (graceful) |
postfix |
/etc/postfix/certintel-fullchain.pem, certintel-key.pem |
root:root |
0600 / 0644 |
postfix check |
postfix reload |
dovecot |
/etc/dovecot/certintel-fullchain.pem, certintel-key.pem |
root:root |
0600 / 0600 |
doveconf -n |
doveadm reload |
postgresql |
/etc/postgresql/certintel-server.crt, certintel-server.key |
postgres:postgres |
0600 / 0644 |
none | systemctl reload postgresql |
mysql |
/etc/mysql/certintel-cert.pem (certificate only), certintel-key.pem, certintel-ca.pem (chain) |
mysql:mysql |
0600 / 0600 |
none | FLUSH SSL on MariaDB (Debian 13's MySQL), ALTER INSTANCE RELOAD TLS on MySQL 8.0.16 and later, both over the local socket, or systemctl restart mysql; then the hook checks that the server's ssl_cert is the installed file |
tomcat |
conf/certintel/$ID-cert.pem, $ID-key.pem, $ID-chain.pem below Tomcat's base folder |
root: Tomcat's group |
0640 / 0640 |
none: server.xml must name the files |
systemctl restart of Tomcat's unit (see Tomcat) |
The files must not be writable by other users; the hook builder refuses such a mode.
Point the service at the files¶
The hook only installs the files. Configure the service once to read them, for example:
| Kind | Configuration |
|---|---|
haproxy |
bind :443 ssl crt <file> (or crt <folder>) |
caddy |
tls <certificate> <key> in the site block |
traefik |
tls.certificates certFile/keyFile in the file provider's configuration in /etc/traefik/dynamic (providers.file.directory); the hook touches it so Traefik reads the renewed files |
litespeed |
certFile, keyFile and certChain 1 in the listener or the virtual host's vhssl block |
postfix |
smtpd_tls_chain_files = <key>, <certificate> in main.cf |
dovecot |
ssl_server_cert_file = <certificate> and ssl_server_key_file = <key> (Dovecot 2.4, as in Debian 13); on 2.3 ssl_cert = <<certificate> and ssl_key = <<key> |
postgresql |
ssl_cert_file and ssl_key_file in postgresql.conf |
mysql |
ssl_cert, ssl_key and ssl_ca in the [mysqld] section, then restart the server once the files exist: MariaDB does not start while they are missing, so restart it after the first certificate; the Agent retries the installation |
Server detection can read the current paths from an existing HAProxy or LiteSpeed configuration and pre-fill them.
Add the TLS site without restarting the service. HAProxy, Caddy and MariaDB do not start while the certificate files they name are missing, and the starter hook reloads a running service (a reload does not start a stopped one): restart such a service only once the first certificate's files exist.
Tomcat¶
The Tomcat (starter hook) workflow (template tomcat-standard, Agent
1.9.0+) gets a certificate, saves it as PEM files in the Agent's folder, and
runs the root starter hook certintel-tomcat.sh, which installs the files for
Tomcat and restarts Tomcat. The Agent then checks that Tomcat's HTTPS port
serves the new certificate. It works with Tomcat 9, 10 and 11, which read PEM
files directly: no keystore, keystore password or Java tool is involved.
What you need:
- the Agent on the Linux server, connected and approved;
- Tomcat 9, 10 or 11 running as a systemd service;
- the name resolves to the server, and Tomcat's HTTPS port is reachable from the server itself.
1. Write the hook, once, on the server:
It writes /etc/certintel/hooks.d/certintel-tomcat.sh (owner root, mode
0700) and prints the connector lines of step 2. The dashboard refuses a
Tomcat proposal until the Agent reports this script installed.
2. Add the connector to server.xml, once. Add these lines inside
<Service name="Catalina"> in conf/server.xml (/etc/tomcat10/server.xml
on Debian and Ubuntu, /etc/tomcat/server.xml on the Red Hat family), with
your HTTPS port and the certificate's id as the dashboard and certintel
status show it:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateFile="conf/certintel/CERTIFICATE-ID-cert.pem"
certificateKeyFile="conf/certintel/CERTIFICATE-ID-key.pem"
certificateChainFile="conf/certintel/CERTIFICATE-ID-chain.pem" />
</SSLHostConfig>
</Connector>
The paths are relative to Tomcat's base folder, so the lines are the same on
every system. The script never edits server.xml. Leave Tomcat running after
adding the lines: the script restarts it.
Where the files go. conf/certintel/ below Tomcat's base folder (on
Debian /etc/tomcat10/certintel/): the owner is root and the group is
Tomcat's group, the files have mode 0640 and the folder 0750, so Tomcat
can read the key and cannot replace it.
What the script finds by itself.
| It needs | Where it looks |
|---|---|
| Tomcat's base folder | /var/lib/tomcat11, /var/lib/tomcat10, /var/lib/tomcat9, /usr/share/tomcat, /opt/tomcat and /usr/local/tomcat: the one that holds conf/server.xml |
| The systemd unit | Named after the base folder: tomcat10, tomcat9, tomcat |
| The account Tomcat runs as | The unit's User=, else tomcat, tomcat11, tomcat10 or tomcat9 |
When it finds none, or the wrong one, set TOMCAT_BASE, TOMCAT_USER or
TOMCAT_UNIT at the top of the script. It stops and says which one to set,
and it stops when it finds two Tomcats.
What restarts. Tomcat does not load a new certificate while it runs, so
the script runs systemctl restart on Tomcat's unit; connections are cut for
the seconds Tomcat takes to start. It restarts Tomcat only when server.xml
names the files, and only after it installed new files or server.xml
changed. Until server.xml names the files it installs them, prints the
connector lines to the journal (journalctl -t certintel-agent-hooks) and
fails the step without a restart.
The first certificate. If the connector is already in server.xml, the
first run installs the files, restarts Tomcat and passes. If it is not, the
first run fails and the dashboard waits for the site: add the lines, and the
Agent tries again every 30 seconds for about 15 minutes, or at once with
sudo certintel deployments retry CERTIFICATE-ID.
Renewal is automatic: new files, one restart, and the Agent confirms that the port serves the new certificate.
The port. Get a certificate for a server uses port 8443. For another port, create the workflow with New workflow and set its HTTPS port; use the same port in the connector.
The keystore templates tomcat-jks and tomcat-p12 are for Windows only. The
Agent does not detect Tomcat on a server; you choose Tomcat (starter
hook) as the server type.
Test status
The Agent's test of this hook runs it against Debian 13's tomcat10
package. Tomcat 9 and 11, the Red Hat family's package and a Tomcat
unpacked from the Apache archive were not tested. Read the generated
script before the first renewal.
In the terminal interface¶
Run sudo certintel tui, press a on the Workflows screen and choose the
service in Type on the deployment page. The page pre-fills the target
paths, owner, group, mode and the test and reload commands from the table
above; you can change each one. A service type always saves an unencrypted,
owner-only key, because the service cannot read a password-protected one.
When you save, the terminal interface prepares the Agent's folder and writes
the starter hook as root. S (post-renewal scripts) also offers each starter
as Install for <Service> (certintel-<kind>.sh).
From the command line¶
certintel hooks starter KIND [--write] prints the starter hook
certintel-KIND.sh for one of haproxy, caddy, traefik, litespeed,
postfix, dovecot, postgresql, mysql or tomcat (and nginx and
apache, which test the configuration and reload the web server; the server
reads the saved files as root), or with --write (as root)
writes it to /etc/certintel/hooks.d and prints Wrote … (root:root, mode
0700). Check the paths, owner and commands in it before the first renewal.
Exit codes: 0 success, 1 the script could not be written (for example
script … already exists; edit it, or remove it first to start again), 2
invalid usage or not root.
Dashboard-assigned workflows¶
The catalogue templates haproxy-standard, caddy-standard,
traefik-standard, litespeed-standard, postfix-standard,
dovecot-standard, postgresql-standard, mysql-standard and
tomcat-standard, shown as
<Service> (starter hook), can be
assigned from the dashboard. Their fields are
Agent folder (the absolute folder the Agent saves the PEM files in, for
example /etc/certintel/certs/haproxy), Filename prefix (default
certificate) and, for HAProxy, Caddy, Traefik and LiteSpeed,
Verification hostname and Verification port (443) for an endpoint
check after the reload. Tomcat has Verification hostname and HTTPS
port (8443).
The starter hook is installed on approval (Agent 1.14.0+)¶
An assignment carries no script. From Agent 1.14.0 the Agent installs its own starter hook when it is missing and a workflow that needs it is approved on the device or applied by the auto-apply policy:
- The service asks the root hook runner for the script by kind. The runner
writes the same
certintel-KIND.shthatsudo certintel hooks starter KIND --writewrites, with its default paths and commands, as root:root with mode 0700. It never replaces an existing script, so one you edited stays. certintel assignments showand the terminal interface's approval question list it before you approve: Installs: certintel-nginx.sh: the agent's own starter hook, written to /etc/certintel/hooks.d (root, mode 0700) when this is approved; it runs as root after each renewal.- The event
starter-hook-installedrecords the script, the assignment and who approved it (a local administrator or the auto-apply policy). - If the script cannot be installed, nothing is changed on the device, the dashboard shows the reason, and Send again proposes it once more.
What this lets a dashboard proposal do
On an Agent with auto-apply on, a proposal from the dashboard alone now
leads to one of the Agent's own scripts running as root after each
renewal: a configuration test and a reload, for some services a restart.
The script is the Agent's and nothing in it comes from the proposal. To
require a command on the server again, turn it off there:
sudo certintel config set install-starter-hooks=off, or Install
starter hooks on the terminal interface's Settings screen. Turning
auto-apply off keeps every proposal waiting for a local approval. Before
a downgrade to an earlier Agent, set install-starter-hooks back to on:
earlier Agents do not start with a configuration that holds the key.
The default scripts suit a standard installation of each service. Where the
service's unit or paths differ (Apache on the Red Hat family is httpd, not
apache2), write the script first with hooks starter KIND --write and edit
it; the Agent then leaves it alone.
Before you assign a template¶
With an earlier Agent, or with install-starter-hooks off, an assignment
cannot create a script and step 1 is needed. Step 2 always applies.
- Install the starter hook:
sudo certintel hooks starter KIND --write, or create the workflow once insudo certintel tui. Until the hook exists the Agent refuses the assignment with starter hook certintel-KIND.sh is not installed; run certintel hooks starter KIND --write as root. The Agent reports whether each template's hook is installed, so the dashboard shows Needs setup on the device on the form and refuses to send the proposal (HTTP 422) until the Agent has reported the hook.hooks starter KIND --writereports it at once; a hook installed another way is reported with the Agent's next check of dashboard assignments (about every 5 minutes), or at once withsudo certintel assignments refresh. - Choose an Agent folder the Agent may write. A folder directly below
/etc/certintel/certs(for example/etc/certintel/certs/haproxy) needs no preparation: the installer gives/etc/certintel/certsto the Agent's account and the service unit allows it, so the Agent creates the folder itself. Any other folder must be prepared once withsudo certintel tui. The Agent refuses the assignment when it arrives, before any certificate is ordered, if the folder is one the service cannot write, or if a file the save step would replace there belongs to root or is a link (the agent's service cannot replace a file it would save in … (… belongs to root); remove that file (the agent then creates its own) or choose another file name prefix).
HAProxy from the dashboard¶
Get a certificate for a server on the dashboard's Overview proposes
haproxy-standard with the Agent folder /etc/certintel/certs/<certificate-id>
(the certificate id is the first name with dots as dashes). For Agent 1.14.0+
with install-starter-hooks on, the page says Nothing to install first and
names the script the Agent installs when the proposal is approved or applied.
Otherwise it gives the commands to run first:
sudo certintel hooks starter haproxy --write
sudo install -d -o root -g haproxy -m 0750 /etc/haproxy/certs
The HAProxy starter hook creates a missing
/etc/haproxy/certs itself (as root:haproxy, mode 0750, when its parent
exists; Caddy's and Traefik's hooks do the same for their folders). The page
shows the hook as the server last reported it (Installed or Not
installed yet): hooks starter haproxy --write reports it to CertIntel at
once, sudo certintel assignments refresh does too, and
the page reads the report again by itself (Check again does so at once);
Propose the certificate appears
once the hook is installed. The hook
installs the combined file /etc/haproxy/certs/<certificate-id>.pem (full chain
and key): exactly the file the bind line below names, so the line needs no
change. The certificate ID is the name the page shows (the hook calls it
$CERTINTEL_CERTIFICATE_ID); the starter hooks list
the files they write in their header comments, with <certificate id> where
the path holds it. The page shows the bind line, with a Copy button,
before you propose. As soon as the Agent has saved the files (within about a
minute of the proposal on a server that applies proposals by itself,
otherwise of your approval), add it to the frontend that serves the site:
and run sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload
haproxy.
On Linux, nginx-standard and apache-standard are built the same way: they save <prefix>-chain.pem and
<prefix>-key.pem (an unencrypted key) in the Agent folder, run
certintel-nginx.sh or certintel-apache.sh as root (nginx -t -q or
apachectl configtest, then systemctl reload), and check that the endpoint
serves the new certificate. Point ssl_certificate / ssl_certificate_key
(NGINX) or SSLCertificateFile / SSLCertificateKeyFile (Apache) at those
two files; the Agent does not edit the server's configuration. Agent 1.14.0+
installs the hook when the workflow is approved; with an earlier Agent
install it first with sudo certintel hooks starter nginx --write (or
apache). Tomcat on Linux uses tomcat-standard (see
Tomcat); the Tomcat JKS and PKCS#12 templates are for Windows
only.
Replacing the files the server already reads¶
(Agent 1.14.0+) nginx-standard and apache-standard have an optional
setting, Certificate file NGINX reads (or Certificate file Apache httpd
reads). Set it to the certificate file the server uses now: the path its
configuration names with ssl_certificate (NGINX) or SSLCertificateFile
(Apache). The Agent then replaces that file with the new certificate and its
chain, and the key file the configuration names with it with the new key,
each time it issues or renews the certificate. You do not change the
server's configuration, and you do not prepare a folder.
When you start a workflow from a server the Agent detected (Create workflow under Detected on this server), the dashboard fills the setting in for you and says so above the form.
What to know:
- Any folder that only root can change works, for example
/etc/letsencrypt/live/www.example.com/fullchain.pemor a file below/root/.acme.sh. The files are replaced by the Agent's root helper (the one that runs the starter hook), not by the Agent's service, which still writes only below/etc/certintel/certs. - The folder must belong to root. The folder that holds each file, and
every folder above it, must be owned by root and not writable by its group
or by others. Otherwise the step fails with folder … can be changed by an
account other than root, so the runner does not write to … as root; nothing
was replaced. Correct the folder (
chown root,chmod go-w), or leave the setting empty. - A server that already reads the Agent's files needs no setting. If the
configuration names files below
/etc/certintel/certs, the server gets every renewal already. Leave the setting empty; the dashboard does not fill it in for such a server. - Only files the server's configuration names are replaced. The Agent reads the NGINX or Apache configuration on the machine each time. If no configuration names the file as a certificate, the workflow step fails with … is not a certificate file the NGINX or Apache configuration on this host names …; nothing was replaced, and no file is written. You never enter the key file: the Agent takes it from the same configuration.
- Only with a certificate for that site. The new certificate must hold at
least one of the names the configuration gives the site (
server_name,ServerName,ServerAlias). A certificate for other names is refused with the new certificate (…) holds none of the names the server that reads … answers for (…), so a workflow for one site cannot replace the certificate of another. A site without a server name takes any certificate. - A certificate that several sites share keeps all their names. When more than one site reads the certificate file, the new certificate must hold every one of their names that the present certificate holds. If it does not, the step fails with the new certificate (…) does not hold …, which the certificate in … holds now for the servers that read it. Add the missing names to the workflow's certificate.
- A key or chain file shared with another certificate is not replaced. If another site's configuration names the same key file or chain file together with a different certificate, the step fails with … is also named by …, together with other certificate or key files: replacing it would break that site.
- The job result says so. The script step of a successful job reads
replaced 2 of the server's certificate files (3 with a separate chain
file). The files are named in
journalctl -t certintel-agent-hooks. - Each new file keeps the owner, group and mode of the file it replaces.
A key file is never left readable by other users. A link, such as the ones
in certbot's
livefolder, is replaced by a regular file; the file it pointed to is left alone. - Apache with
SSLCertificateChainFile: the certificate file gets the certificate alone and the chain file gets the chain. Otherwise the certificate file gets the certificate followed by its chain. A certificate that comes without an issuing chain is refused while the configuration names a chain file. - Stop the tool that renewed the certificate before (certbot, acme.sh), or it will put its own certificate back at its next renewal.
- The Agent keeps its own copy in the Agent folder, as before.
- Leave the setting empty to keep the earlier behaviour: the Agent saves the files in the Agent folder and you point the server at them.
The setting is for NGINX and Apache httpd. The other starter hooks (HAProxy, LiteSpeed and the rest) install the files at the paths written in their own script.