I am using Traefik 1.7 for APIs serving mobile apps that require to use certificate pinning to the public key, and I don't find anywhere in the docs how I can configure acme to reuse the same public key when renewing the certificates, I believe that would be like the --reuse-key in certbot:
--reuse-key When renewing, use the same private key as the existing certificate. (default: False)
If we look at the code in Traefik 1.7, you can see that we are passing the key pair to the renewer, so by all accounts you should be able to use the servers public key that was generated when the keypair was initially generated.
What cert/key are you using when configuring your mobile client?
In order to troubleshoot this further in Traefik, we would need some way to reproduce the issue and confirm that the private/public keypairs are in fact being regenerated on renewal.
So the mobile app only contains an hash of the public key of the first LetsEncrypt certificate, and I expect that on renewals the certificate public key doesn't change, but I don't put any cert/key inside the mobile app.
Nice to know that's already supported, but I am not familiar with GoLang, neither with Traefik, thus I would like to know if I need to change anything in my Traefik configuration for that code you linked to work as its supposed to work?
NOTE: I only have issues when I put the API backend behind Traefik.
What do you need more than the what I provided in the topic?
In your acme.json file, which of these have you observed changing when the cert is up for renewal?
certA is the CN, which should be the short-lived cert, this is going to change on renewal
certB is the CA/Trust chain, I believe, and it has a longer duration 300 days. I'm not sure if this changes or not
rsaPrivateKey should not change, and I would assume this is what should be hashed
In order to recreate, I'm suspect functionality would need to be added to a forked Traefik to force a renewal, and then steps provided to reproduce (your environment, are you using DNS/HTTP/TLS challenge)
Also, I am under the assumption that you are base64 decoding these before hashing, I'm guessing the pinning wouldn't even work if you weren't.
Sorry but I cannot debug this because I don't have a copy of the old acme.json.
As I said pinning is done to the public key, not to the private key.
From the article I linked previously:
The easiest way to pin is to use the server’s public key or the hash of that public key, The hashed public key is the most flexible and maintainable approach since it allows certificates to be rotated in the server, by signing the new one with the same public key. Thus the mobile app does not have to be updated with a new pin because the hash for the public key of the new certificate will continue to match the pin provided in the network security config file.
Not sure how to implement this force renewal, but I will try to setup a staging Traefik setup and keep a copy from the original acme.json, so that I can then compare with one that is updated after renewal.
For now I disabled acme in Traefik and I am using instead certbot to handle certificates creation and renewal.
Using http challenge. Here it his my acme configuration:
Yes it's base64 encoded, as you can see from my previous screenshot, and you can try it yourself with this example script to extraxt it from a domain name:
#!/bin/bash
# Heavily inspired on:
# * https://medium.com/@appmattus/android-security-ssl-pinning-1db8acb6621e#ecea
set -eu
Main()
{
local domain="${1? Missing domain name to extract and hash the certificate public key !!!}"
local domain="${domain##*://}"
local certs=$( openssl s_client -servername "${domain}" -host "${domain}" -port 443 -showcerts </dev/null 2>/dev/null | sed -n '/Certificate chain/,/Server certificate/p' )
local rest=$certs
while [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]]; do
cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----"
rest=${rest#*-----END CERTIFICATE-----}
local certificate_name="$( echo "$cert" | grep 's:' | sed 's/.*s:\(.*\)/\1/' )"
if [ -n "${certificate_name}" ]; then
printf "\nCERTIFICATE NAME:\n\n${certificate_name} \n\n"
fi
printf "\nCERTIFICATE PUBLIC KEY HASH:\n\n"
echo "$cert" |
openssl x509 -pubkey -noout |
openssl rsa -pubin -outform der 2>/dev/null |
openssl dgst -sha256 -binary |
openssl enc -base64
echo
exit 0
done
}
Main ${@}
Thank you for the detailed response. I was able to determine that Traefik 1.7/2.x (the renewal code is identical in both versions) will renew the cert using the same RSA secret, and in effect, retain the public key signature.
I have created a fork of Traefik v2 which can be used to confirm this behavior, unfortunatly, the acme configuration handler in v1 is not as easy to manipulate as v2 so I did not have the time to experiment with that version.
You can pull this from kcmastrpc/traefik-experimental:v2-force-renew, and adding "ForceRenew": true as in this example:
Thanks for your time in putting the fork with the force renew for me to test.
I gave it a quick run, but I was not able to get Traefik to start with my 1.7 setup and for now I don't have time to allocate for converting my 1.7 setup to what Traefik 2 requires. I will revisit this when I can.
Sorry of not being able to go further at this time.
But based on your degree of confidence I will keep Traefik 1.7 running and in 2 months I will confirm if the renewal of certificates keep the same public key.
After I have seen your code in the branch add-forceRenew and your request to try it with "ForceRenew": true, I realized that I may had messed things up in my setup in March, aka when LetsEncrypted resolved to scrap from one day to the other millions of certificates due to a bug in their code. On the time I arrived to work and saw an email alerting me that in a few hours I would have my certificates revoked, thus I rushed to hack a way around forcing the renewal of the certificates, and that may have been the cause of the failure of the public key not matching in renewals, but I don't recall now exactly how I have done it