Custom actions with Virtualmin's Letsencypt renewals?

Are there any guides or threads with details on how to configure Virtualmin’s Letsencypt script to run custom commands when any certs are renewed?

I just need to copy the certs to:

	DOMAIN='NAMEHERE.com' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/certs/$DOMAIN.pem'
	systemctl restart haproxy

Or should I just look-up the standard letsencrypt guides and then Virtualmin will automatically pick up those commands?

Definitely not this. Virtualmin calls certbot, and then does stuff with the resulting certs, not the other way around.

Virtualmin has pre and post hooks, though, so you can write scripts in whatever language you like (including bash shell scripts) to run before or after changes. Renewing certs is a change, and the hook runs. To know if it’s a TLS cert change, you’d check for SSL_DOMAIN.

There’s an example of copying certs for a service not supported directly by Virtualmin here: Domain Management API | Virtualmin — Open Source Web Hosting Control Panel

So, you could base your post script on that, and then configure Virtualmin to run it in Virtualmin Configuration->Actions upon server and user creation->Command to run after making changes to a server

2 Likes

Thanks Joe!

So to confirm I’d need to:

Create script copy-cert-to-haproxy.sh:

#!/bin/bash
if [ "$VIRTUALSERVER_ACTION" = "SSL_DOMAIN" ] | [ "$VIRTUALSERVER_ACTION" = "CREATE_DOMAIN" ]; then
  /usr/bin/cat "/home/$VIRTUALSERVER_DOM/ssl.combined" "/home/$VIRTUALSERVER_DOM/ssl.key" > "/etc/haproxy/certs/$VIRTUALSERVER_DOM.pem"
  /usr/bin/systemctl restart haproxy.service
fi

Or:

#!/bin/bash
if [ "$VIRTUALSERVER_ACTION" = "SSL_DOMAIN" ] | [ "$VIRTUALSERVER_ACTION" = "CREATE_DOMAIN" ]; then
  /usr/bin/cp -f "/home/$VIRTUALSERVER_DOM/ssl.everything"  "/etc/haproxy/certs/$VIRTUALSERVER_DOM.pem"
  /usr/bin/systemctl restart haproxy.service
fi

Upload it to my usual scripts directory, and then in System Settings > Virtualmin Configuration put the path . /home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh into Command to run after making changes to a server?

If so that’s much easier than I thought it would be! :smiling_face:

You need a shebang (e.g. #!/bin/sh or #!/bin/bash or whatever), but otherwise looks fine.

I’d probably also recommend wrapping anything with a variable in double quotes. I need to fix that example. I don’t know who wrote it, but I try to make all my examples pass shellcheck, so it couldn’t have been me. See some of the other examples for better practice in terms of using variables in shell scripts. (And, use shellcheck to get good advice about safer and more reliable shell scripting.)

1 Like

Thanks Joe, I’ve edited my post above yours with the updated snippet - do you mean like that?

Unfortunately it did not work. I created a new test.domain.com virtualserver and Virtualmin set it up as usual, it fetched the letsencrypt script and ran copy-cert-to-haproxy.sh - however the resulting file is empty:

-rw-r--r-- 1 root root    0 Dec 25 03:20 test.domain.com.pem

I changed System Settings > Virtualmin Configuration > Command to run after making changes to a server? to bash /home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh (added bash to the start) as I was getting permission denied without it, but something is still amiss.

Any idea what might be wrong here?

No, I mean wrap the whole string in quotes. Substitution happens anywhere within with double quotes.

e.g.

 /usr/bin/cat "/etc/ssl/virtualmin/$VIRTUALSERVER_ID/$VIRTUALSERVER_DOM.combined" "/etc/ssl/virtualmin/$VIRTUALSERVER_ID/$VIRTUALSERVER_DOM.key" > "/etc/haproxy/certs/$VIRTUALSERVER_DOM.pem"
1 Like

Are you still getting permission denied, or something else?

You need to make the file executable, if you haven’t already. You can test your script by setting the variables you use in the script before you run it. e.g.:

# export VIRTUALSERVER_ACTION='SSL_DOMAIN'
# export VIRTUALSERVER_DOM='domain.tld'
# export VIRTUALSERVER_ID='domain ID'
# ./copy-cert-to-haproxy.sh
1 Like

speaking in general, is it possible to adjust the software package to look in /etc/letsencrypt/live/$DOMAIN/ and directly use the certs from that location?

1 Like

Nice idea, however HAProxy only allows you to specify a directory, so they’d all need to in there (for each domain) plus I’d still need to restart HAProxy. Think the inbuilt hooks Joe mentioned is probably the best option for this case :smiley:

I updated the script to:

if [ "$VIRTUALSERVER_ACTION" = "SSL_DOMAIN" ]; then
  /usr/bin/cat "/home/$VIRTUALSERVER_DOM/ssl.combined" "/home/$VIRTUALSERVER_DOM/ssl.key" > "/etc/haproxy/certs/$VIRTUALSERVER_DOM.pem"
  /usr/bin/systemctl restart haproxy.service
fi

And it works when I do this via terminal:

But not when I create a new test.domain.com - Virtualmin reports it has created everything successfully but nothing gets copied over to /etc/haproxy/certs/ - the domain creation page does not report a fault:

Creating administration group test.domain.com ..
.. done

Creating administration user test.domain.com ..
.. done

Creating aliases for administration user ..
.. done

Adding administration user to groups ..
.. done

Creating home directory ..
.. done

Creating mailbox for administration user ..
.. done

Adding new DNS zone ..
.. done

Adding new virtual website ..
.. done

Adding webserver user apache to server's group ..
.. done

Performing other Apache configuration ..
.. done

Creating SSL certificate and private key ..
.. done

Adding new SSL virtual website ..
.. done

Saving server details ..
.. done

Re-starting DNS server ..
.. done

Applying web server configuration ..
.. done

Re-starting Webmin ..
.. done

Re-starting Usermin ..
.. done

Requesting a certificate for test.domain.com, www.test.domain.com from Let's Encrypt ..
.. request was successful!

Creating initial website index page ..
.. done

Applying web server configuration ..
.. done

Re-starting Webmin ..
.. done

Re-starting Usermin ..
.. done

In the field for Command to run after making changes to a server I have tried both:

bash /home/mydomain/scripts/copy-cert-to-haproxy.sh
as well as without bash at the start:
/home/mydomain/scripts/copy-cert-to-haproxy.sh

But neither of these seem to work.

Summary: testing via the command line works, but not when virtualmin creates the account. Any ideas what to try next?

Hmm…I’m not sure how to troubleshoot that.

Maybe watch the Webmin actions log when you renew a certificate. Just tail -f /var/webmin/webmin.log

Doesn’t appear to be any clues in the log Joe:

1735161898.12567.0 [25/Dec/2024 21:24:58] - - - webmin letsencrypt-dns.pl "letsencryptdns" "-" "test.domain.com"
1735161912.12783.0 [25/Dec/2024 21:25:12] - - - webmin letsencrypt-dns.pl "letsencryptdns" "-" "www.test.domain.com"
1735161920.13019.0 [25/Dec/2024 21:25:20] - - - webmin letsencrypt-cleanup.pl "letsencryptcleanup" "-" "test.domain.com"
1735161923.13060.0 [25/Dec/2024 21:25:23] - - - webmin letsencrypt-cleanup.pl "letsencryptcleanup" "-" "www.test.domain.com"
1735161936.11219.0 [25/Dec/2024 21:25:36] root 803a9a09bf581082ed80daadf3484676 102.22.83.84 virtual-server domain_setup.cgi "create" "domain" "test.domain.com" alias='' alias_mode='0' aliasdomslimit='*' aliaslimit='' aliasmail='' allowedscripts='' auto_letsencrypt='1' bw_limit='' cgi_bin_dir='cgi-bin' cgi_bin_path='/home/test.domain.com/cgi-bin' created='1735161868' creator='root' crypt_enc_pass='IRrSQZzFN/Qh2' db='test_domain_com' db_mysql='' db_postgres='' dbslimit='' defip='1' digest_enc_pass='f275d1687cd464cebf2466bb4852d0de' dir='1' dns='1' dns_ip='3.11.102.75' dom='test.domain.com' domslimit='*' edit_admins='1' edit_aliases='1' edit_allowedhosts='1' edit_backup='1' edit_catchall='1' edit_dbs='1' edit_delete='1' edit_disable='1' edit_dnsip='1' edit_domain='1' edit_forward='1' edit_ip='1' edit_mail='1' edit_passwd='1' edit_phpmode='1' edit_phpver='1' edit_records='1' edit_redirect='1' edit_restore='1' edit_sched='1' edit_scripts='1' edit_sharedips='1' edit_spam='1' edit_spf='1' edit_ssl='1' edit_users='1' email='' emailto='test.domain.com@root.server.com' emailto_addr='test.domain.com@root.server.com' emailto_src='test.domain.com@root.server.com' enc_pass='$6$3516186d0$66/Mhv6rR9xbjpGSGzweUNMj7TaleaeDC6WdDSE5B/bm7kYXSoggt4b1n1PKPzSpf.bBebd37.dvF2xv4EnJ.' forceunder='0' ftp='0' gid='1043' group='test.domain.com' hashpass='1' home='/home/test.domain.com' id='173513385911219' ip='3.11.102.75' ip6='' ipfollow='' lastsave='1735161932' letsencrypt_dname='' letsencrypt_dwild='0' letsencrypt_last='1735161930' letsencrypt_renew='1' limit_dir='1' limit_dns='1' limit_ftp='0' limit_logrotate='0' limit_mail='0' limit_mysql='0' limit_postgres='0' limit_spam='0' limit_ssl='1' limit_unix='1' limit_virt='0' limit_virus='0' limit_web='1' limit_webalizer='0' limit_webmin='0' logrotate='0' mail='0' mailboxlimit='' md5_enc_pass='$1$35161860$Pjft0K7.LbB7.iz/kq5PZ.' migrate='1' mongrelslimit='' mysql='0' mysql_module='' mysql_pass='rEChAwM14rK9JSs' name='1' name6='1' netmask='' netmask6='' nodbname='0' norename='0' owner='' parent='' plan='0' postgres='0' prefix='test.domain.com' proxy_pass='' proxy_pass_mode='0' public_html_dir='public_html' public_html_path='/home/test.domain.com/public_html' quota='' realdomslimit='*' reseller='' safeunder='0' source='domain_setup.cgi' spam='0' ssl='1' ssl_cert='/home/test.domain.com/ssl.cert' ssl_cert_expiry='1742934412' ssl_chain='/home/test.domain.com/ssl.ca' ssl_combined='/home/test.domain.com/ssl.combined' ssl_everything='/home/test.domain.com/ssl.everything' ssl_key='/home/test.domain.com/ssl.key' ssl_pass='' subdom='' subprefix='' template='0' ugid='1043' ugroup='test.domain.com' uid='1060' unix='1' uquota='' user='test.domain.com' virt='0' virt6='' virt6already='' virtalready='0' virus='0' web='1' web_port='8080' web_sslport='4443' web_urlport='' web_urlsslport='' webalizer='0' webmin='0'

I am guessing there are no variables available to the script, as previously I had taken out the if [ "$VIRTUALSERVER_ACTION" = "SSL_DOMAIN" ]; then conditional and that’s when it was giving me an empty file. Either that or $VIRTUALSERVER_ACTION" = "SSL_DOMAIN" is not being set on domain creation?

Only other thing I can think of is the command to run the script might need to be different, so far I have tried all of the below:

/home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh
. /home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh
bash /home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh

I know that bash /home/mymaindomain.com/scripts/copy-cert-to-haproxy.sh definitely runs the script as when I removed the condition that’s when I was getting an empty file - so I think it’s either the variable isn’t available or the correct one (SSL_DOMAIN) is not being set on domain creation?

So, check all the variables you’re expecting by logging the env when it runs and then exit. Modify your script to have this at the beginning, just after the shebang:

env >> /home/domain.tld/copy-cert-to-haproxy.log
exit

Then see what shows up in that log. Make sure all the variables are set how you think. (There will probably be other unrelated stuff in there, but you can search for the variables you want.)

1 Like

Thanks Joe!

The problem was VIRTUALSERVER_ACTION=CREATE_DOMAIN so I changed the conditional to:

if [ "$VIRTUALSERVER_ACTION" = "SSL_DOMAIN" ] | [ "$VIRTUALSERVER_ACTION" = "CREATE_DOMAIN" ]; then

If VIRTUALSERVER_ACTION is always SSL_DOMAIN on cert renewal then the updated script should ensure it will always works - or do I need to look for other possible variants for VIRTUALSERVER_ACTION as well?

Another related question:

If we go to Virtualmin (domain/account) > Server Configuration > SSL Certificate and request or renew a certificate should this also trigger the Command to run after making changes to a server action?

Asking because I just did that and it did not. It’s renewed the certificate but not copied it over to /etc/haproxy/certs/ - so doesn’t appear to have run the Command to run after making changes to a server.

That may be a bug. It should run after renewals, too.

I thought I traced through the code to confirm actions run on all TLS events, but I may have misunderstood the execution path.

@Jamie, shouldn’t that hook run after a Let’s Encrypt renewal, too?

2 Likes

If it’s any help this is what I did:

certbot certificates (to view certs)
sudo certbot delete --cert-name domain.tld (to remove the cert I want Virtualmin to handle)
domain.tld > Edit Virtual Server > Check: Apache SSL website enabled? > Save (and when that did not work I did the below)

domain.tld/Server Configuration > SSL Certificate > Request Certificate

Yes it should do, although until the most recent 7.30.x series release of Virtualmin there was a bug where post-domain actions weren’t run in this case.

2 Likes

That explains it - this server is still on 6.x.

Will post-actions still run (even pre 7.30.x) after the virtualmin renewal script runs? (i.e if Automatically renew certificate? has been checked in Virtualmin > Server Configuration > SSL Certificate > Let's Encrypt) or will it not work at even for that for anything prior to 7.30?

:exploding_head:

Why!?

2 Likes