Auto-renew letsencrypt cert with standard library code

191 views
Skip to first unread message

Michael Ellis

unread,
Nov 5, 2019, 12:20:11 PM11/5/19
to golang-nuts

        I have the code at the bottom of this message in a web server I'm running in a Digital Ocean Droplet.  The app is a simple ear training program for instrumentalists.  The URL is https://etudes.ellisandgrant.com.

        It works with no problems until the letsencrypt certificate expires every 90 days.  ListenAndServeTLS() returns an error, the program exits and restarts (because I'm running under `entr - r`) and then falls into the default case which is plain http service.  I'd like to prevent that since modern browsers (for very good reasons) show scary warnings about plain http sites.  

        I don't need absolute 100% uptime for the program.  A few minutes unavailability while the cert is renewed would be perfectly acceptable.  I just want to add a check at the restart to detect that the cert is expired and renew it automatically.  How can I do that with packages from the Go standard library?  ( I know Caddy is available but I'd prefer not to add a third-party dependency for what seems like a relatively simple task.)

<SNIP>
        var serveSecure bool
var certpath, certkeypath string
if hostport == ":443" {
certpath, certkeypath, err = getCertPaths()
if err != nil {
log.Printf("Can't find SSL certificates: %v", err)
hostport = ":80"
}
serveSecure = true
}
log.Printf("serving on %s\n", hostport)
switch serveSecure {
case true:
if err := http.ListenAndServeTLS(hostport, certpath, certkeypath, nil); err != nil {
log.Fatalf("Could not listen on port %s : %v", hostport, err)
}
default:
if err := http.ListenAndServe(hostport, nil); err != nil {
log.Fatalf("Could not listen on port %s : %v", hostport, err)
}
}
</SNIP>

/ getCertPaths attempts to retrieve a certficate and key for use with
// ListenAndServeTLS. It returns an error if either item cannot be found but
// does not otherwise attempt to validate them. That is left up to
// ListenAndServeTLS.
func getCertPaths() (certpath string, keypath string, err error) {
certpath = os.Getenv("IETUDE_CERT_PATH")
if certpath == "" {
err = fmt.Errorf("no environment variable IETUDE_CERT_PATH")
return
}
keypath = os.Getenv("IETUDE_CERTKEY_PATH")
if keypath == "" {
err = fmt.Errorf("no environment variable IETUDE_CERTKEY_PATH")
return
}
return
}

Sean Liao

unread,
Nov 5, 2019, 2:17:54 PM11/5/19
to golang-nuts
1. Check certificate expiry (stdlib)
2. Implement ACME client to request certificate
3. Respond to a challenge (the http one is easy)
4. Restart server with new certificate

Kurtis Rader

unread,
Nov 5, 2019, 2:21:12 PM11/5/19
to Michael Ellis, golang-nuts
FWIW, The Caddy web server is written in Go and handles this scenario. So you might consider using it or at least looking at its source to understand how to implement this feature.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/dc40264f-5314-496b-9069-81acbf94701a%40googlegroups.com.


--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Marko Ristin-Kaufmann

unread,
Nov 5, 2019, 2:55:35 PM11/5/19
to Kurtis Rader, Michael Ellis, golang-nuts
Hi,

FWIW, The Caddy web server is written in Go and handles this scenario. So you might consider using it or at least looking at its source to understand how to implement this feature.

We implemented an alternative in case you need more examples: 

Cheers,
Marko

golangguy

unread,
Nov 6, 2019, 3:02:11 AM11/6/19
to golang-nuts
May be , 

I hope you use letusencryt auto renew and running this under cron , 
If you dont want to add any other library , use can put it as a systemctl service,  restart when exits using the service. That might work

Michael Ellis

unread,
Nov 6, 2019, 12:28:06 PM11/6/19
to golang-nuts
Thanks Sean Liao, Kurtis Rader, Marko Ristin-Kaufman, and cg-guy.  Apologies for not responding sooner.

I looked at the code for Caddy and revproxyry.  Neat stuff for sure and I'd seriously consider them for a more elaborate project.

Thinking through my needs I concluded that it's probably better not to embed the process of obtaining and renewing certificates in the infinite-etudes code.  That way if someone chooses to run their own instance, they can do whatever makes the most sense within on their choice of host platform.

So I ended up biting the bullet and learning how to use systemd.  The unit file I ended up with is below. It sets Restart=always to ensure that the program reloads no matter what and uses ExecStartPre to attempt to renew the certificate before starting the server.  Seems reliable across reboots and killing the infinite-etudes process.  I won't know for sure until the cert actually expires in month or so but the logs show that certbot is being invoked whenever the service reloads.  It detects that the certs are unexpired and returns success on exit.

[Unit]
Description=Infinite Etudes server
After=network.target

[Service]
Type=simple
User=mellis
WorkingDirectory=/home/mellis/ietudes
# Always attempt to renew the certificate before (re)starting infinite-etudes
ExecStartPre=+/usr/bin/certbot renew
# infinite-etudes needs two environment variables that give full paths to the certificate
# fullchain and key files.
Environment="IETUDE_CERT_PATH=/etc/letsencrypt/live/etudes.ellisandgrant.com/fullchain.pem"
Environment="IETUDE_CERTKEY_PATH=/etc/letsencrypt/live/etudes.ellisandgrant.com/privkey.pem"
# run infinite-etudes as an https server
ExecStart=/home/mellis/go/bin/infinite-etudes -s -p :443
# Ensure that the process is always restarted on failure or if terminated by a signal
# A 5 second restart delay is used to reduce the possibility of thrashing if
# something is badly wrong.
Restart=always
RestartSec=5

[Install]

WantedBy=multi-user.target

Thanks, again, for the help.

Marko Ristin-Kaufmann

unread,
Nov 6, 2019, 1:53:38 PM11/6/19
to Michael Ellis, golang-nuts
Hi Michael,

 
So I ended up biting the bullet and learning how to use systemd.  The unit file I ended up with is below. 

Given how complex your file is, let me suggest you a simpler alternative: use either caddy or revproxyry as a reverse proxy. You start two processes, the reverse proxy and your service, and just point the reverse proxy to your service. 

Since reverse proxy is just a binary executable, the deployment is just scp + ~5-10 lines config file + a super simple systemd config file.

Cheers,
Marko


Michael Ellis

unread,
Nov 6, 2019, 4:11:56 PM11/6/19
to golang-nuts



Given how complex your file is, let me suggest you a simpler alternative: use either caddy or revproxyry as a reverse proxy. You start two processes, the reverse proxy and your service, and just point the reverse proxy to your service. 

Since reverse proxy is just a binary executable, the deployment is just scp + ~5-10 lines config file + a super simple systemd config file.


Thanks, Marko, but I'm not sure that's simpler.  My unit file has exactly one line devoted to cert renewal.

ExecStartPre=+/usr/bin/certbot renew

All the rest is what's needed to run and  restart infinite-etudes under systemd.

Cheers,
Mike



 

Marko Ristin-Kaufmann

unread,
Nov 6, 2019, 5:47:19 PM11/6/19
to Michael Ellis, golang-nuts
Hi Mike,

Given how complex your file is, let me suggest you a simpler alternative: ...


Thanks, Marko, but I'm not sure that's simpler.  My unit file has exactly one line devoted to cert renewal.

ExecStartPre=+/usr/bin/certbot renew

After a second look on a wider screen, I stand corrected -- your approach is indeed simpler. Please apologize the noise.

Cheers,
Marko


Reply all
Reply to author
Forward
0 new messages