Using Google's Public Certificate Authority with Golang autocert
- 4 minutes read - 744 wordsLast year, I wrote about using Automatic Certs w/ Golang gRPC service on Compute Engine. That solution uses ACME with (the wonderful) Let’s Encrypt. Google is offering a private preview of Automate Public Certificates Lifecycle Management via RFC 8555 (ACME) and, because I’m using Google Cloud Platform extensively to build a “thing” and I think it would be useful to have a backup to Let’s Encrypt, I thought I’d give the solution a try. You’ll need to sign-up for the private preview, for what follows to work.
If you’re content to use certbot
or have a Kubernetes cluster with cert-manager
, the documentation describes configuring these.
Unfortunately, the preview doesn’t describe other use-cases and, not being a security aficionado, it’s always time-consuming for me to understand both the vernacular and the technologies with security-oriented solutions.
After some Googling and stumbling upon issue 48809: Support External Account Binding (EAB) tokens, I’ve been able to get my earlier example working with Google’s public CA using ACME and autocert
and thought I’d share here for others’ benefit. One outstanding question is whether my solution is more complex than it needs to be. I’ll explain below.
Google’s documentation explains how to acquire External Account Binding (EAB) key ID and HMAC. You’ll need to generate these to continue. For convenience, I’m providing these values to my code through the environment and will refer to them by these variables names:
EABKEYID="[Your EAB Key ID]"
EABHMACKEY="[Your EAB HMAC Key]"
NOTE The EAB HMAC Key is Base64 URL-encoded and must be decoded before use.
I added these two variables as flags in my code:
host = flag.String("host", "", "gRPC server's hostname")
port = flag.Uint("port", 443, "gRPC port")
path = flag.String("path", "", "Folder location for certificate")
eabKeyID = flag.String("eab-key-id", "", "")
eabHMACKey = flag.String("eab-hmac-key", "", "") // Base64 URL-encoded
And then:
b64DecodedKey, _ := base64.RawURLEncoding.DecodeString(*eabHMACKey)
The autocert.Manager
now includes an ExternalAccountBinding
field:
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(*path),
HostPolicy: autocert.HostWhitelist(*host),
Client: client,
Email: email,
ExternalAccountBinding: eab,
}
And this is created simply:
eab := &acme.ExternalAccountBinding{
KID: *eabKeyID,
Key: []byte(b64DecodedKey),
}
Google’s documentation explains that, to register an ACME account (certbot register
) and to request certificates (certbot certonly
), a --server
flag is required, defined to be the ACME directory URL for, in this case, Google’s production or staging environments.
For autocert
, the DirectoryURL
is specified on a acme.Client
and so I created one of those:
key, _ := rsa.GenerateKey(rand.Reader, 2048)
production := "https://dv.acme-v02.api.pki.goog/directory"
client := &acme.Client{
Key: key,
DirectoryURL: production,
KID: acme.KeyID(b64DecodedKey),
}
acme.Client
requires a Key
and there’s an example provided in the documentation so I used that.
I noticed that there’s also KID
field and so, even though I’m providing the KID
through the ExternalAccountBinding
on the autocert.Manager
, I added it here too (necessary?)
Lastly, and possibly duplicatively (!?), in the autocert
commit, I noticed that they referenced the ExternalAccountBinding
in the acme.Account
too. So I added it there too!
account := &acme.Account{
Contact: []string{
fmt.Sprintf("mailto:%s", email),
},
ExternalAccountBinding: eab,
}
if _, err := client.Register(
context.Background(),
account,
autocert.AcceptTOS,
); err != nil {
log.Println("unable to register account")
}
The Compute Engine instances create command needs to be updated with the new flags:
PROJECT="[Your GCP Project ID]"
INSTANCE="[Your Compute Engine instance name]"
ZONE="[Your preferred GCP Zone]"
IMAGE="[Your Artifact Registry Repo]"
HOST="${INSTANCE}.[Your Domain]"
PORT="443"
EABKEYID="[Your EAB Key ID]"
EABHMACKEY="[Your EAB HMAC Key]"
TAGS="allow-publicca"
gcloud compute instances create-with-container ${INSTANCE} \
--container-image=${IMAGE} \
--container-arg=--host=${HOST} \
--container-arg=--port=${PORT} \
--container-arg=--path=/certs \
--container-arg=--eab-key-id=${EABKEYID} \
--container-arg=--eab-hmac-key=${EABHMACKEY} \
--tags=${TAGS} \
--machine-type=e2-micro \
--image-family=cos-stable \
--image-project=cos-cloud \
--container-mount-host-path=mount-path=/certs,host-path=/tmp/certs,mode=rw \
--zone=${ZONE} \
--project=${PROJECT}
gcloud compute firewall-rules create allow-${INSTANCE} \
--target-tags=${TAGS} \
--allow=tcp:80,tcp:443 \
--direction=IN \
--project=${PROJECT}
Or you can pull the image for e.g. Artifact Registry and run the container on an instance:
docker pull ${IMAGE}
docker run \
--interactive --tty --rm \
--publish=80:80/tcp --publish=443:443/tcp \
--volume=/tmp/certs:/certs \
${IMAGE} \
--host=${HOST} \
--port=${PORT} \
--eab-key-id=${EABKEYID} \
--eab-hmac-key=${EABHMACKEY}
Once you have an external IP for the instance, update your DNS
IP=$(\
gcloud compute instances describe ${INSTANCE} \
--zone=${ZONE} \
--project=${PROJECT} \
--format="value(networkInterfaces[0].accessConfigs[0].natIP)") && \
echo ${IP}
You should be able to then:
openssl s_client -showcerts -connect ${HOST}:${PORT} 2> /dev/null \
| openssl x509 --noout -dates
And:
grpcurl \
-proto health.proto \
${HOST}:${PORT} \
grpc.health.v1.Health/Check
And, of course, ACME should (!?) deliver a certificate to your instance which will persist it in the container’s /certs
which maps to the instances /tmp/certs
from where you can SCP it somewhere for backup.
If you have recommendations on simplifying the above, please let me know.
That’s all!