Switch back to Let's Encrypt again & implement renewal

This commit is contained in:
Moritz Marquardt 2021-11-20 19:36:12 +01:00
parent 77321eb181
commit b19a5ecc1d
No known key found for this signature in database
GPG key ID: D5788327BEE388B6
2 changed files with 105 additions and 44 deletions

View file

@ -6,7 +6,8 @@
- `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance. - `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance.
- `REDIRECT_BROKEN_DNS` (default: https://docs.codeberg.org/pages/custom-domains/): info page for setting up DNS, shown for invalid DNS setups. - `REDIRECT_BROKEN_DNS` (default: https://docs.codeberg.org/pages/custom-domains/): info page for setting up DNS, shown for invalid DNS setups.
- `REDIRECT_RAW_INFO` (default: https://docs.codeberg.org/pages/raw-content/): info page for raw resources, shown if no resource is provided. - `REDIRECT_RAW_INFO` (default: https://docs.codeberg.org/pages/raw-content/): info page for raw resources, shown if no resource is provided.
- `ACME_API` (default: https://acme.zerossl.com/v2/DV90): set this to https://acme.mock.director to use invalid certificates without any verification (great for debugging). ZeroSSL is used as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt). - `ACME_API` (default: https://acme-v02.api.letsencrypt.org/directory): set this to https://acme.mock.director to use invalid certificates without any verification (great for debugging).
ZeroSSL might be better in the future as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt), but I couldn't get it to work yet.
- `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider. - `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider.
- `ACME_EAB_KID` & `ACME_EAB_HMAC` (default: don't use EAB): EAB credentials, for example for ZeroSSL. - `ACME_EAB_KID` & `ACME_EAB_HMAC` (default: don't use EAB): EAB credentials, for example for ZeroSSL.
- `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider. - `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider.

View file

@ -8,6 +8,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/gob"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/OrlovEvgeny/go-mcache" "github.com/OrlovEvgeny/go-mcache"
@ -88,22 +89,7 @@ var tlsConfig = &tls.Config{
var tlsCertificate tls.Certificate var tlsCertificate tls.Certificate
var err error var err error
var ok bool var ok bool
if tlsCertificate, ok = retrieveCertFromDB(sniBytes); ok { if tlsCertificate, ok = retrieveCertFromDB(sniBytes); !ok {
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil {
panic(err)
}
if !bytes.Equal(sniBytes, MainDomainSuffix) && !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(-7 * 24 * time.Hour)) {
go (func() {
tlsCertificate, err = obtainCert(acmeClient, []string{sni})
if err != nil {
log.Printf("Couldn't renew certificate.")
}
})()
}
}
if tlsCertificate.Certificate == nil || !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(-5 * time.Minute)) {
// request a new certificate // request a new certificate
if bytes.Equal(sniBytes, MainDomainSuffix) { if bytes.Equal(sniBytes, MainDomainSuffix) {
return nil, errors.New("won't request certificate for main domain, something really bad has happened") return nil, errors.New("won't request certificate for main domain, something really bad has happened")
@ -114,7 +100,7 @@ var tlsConfig = &tls.Config{
return nil, err return nil, err
} }
tlsCertificate, err = obtainCert(acmeClient, []string{sni}) tlsCertificate, err = obtainCert(acmeClient, []string{sni}, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -196,6 +182,13 @@ func newAcmeClient(configureChallenge func(*resolver.SolverManager) error) *lego
var acmeClient, mainDomainAcmeClient *lego.Client var acmeClient, mainDomainAcmeClient *lego.Client
var acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{} var acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15 * time.Minute)
// rate limit is 20 / second, we want 10 / second
var acmeClientRequestLimit = equalizer.NewTokenBucket(10, 1 * time.Second)
type AcmeTLSChallengeProvider struct{} type AcmeTLSChallengeProvider struct{}
var _ challenge.Provider = AcmeTLSChallengeProvider{} var _ challenge.Provider = AcmeTLSChallengeProvider{}
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error { func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
@ -208,29 +201,50 @@ func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
func retrieveCertFromDB(sni []byte) (tls.Certificate, bool) { func retrieveCertFromDB(sni []byte) (tls.Certificate, bool) {
// parse certificate from database // parse certificate from database
certPem, err := keyDatabase.Get(sni) resBytes, err := keyDatabase.Get(sni)
if err != nil { if err != nil {
// key database is not working // key database is not working
panic(err) panic(err)
} }
if certPem == nil { if resBytes == nil {
return tls.Certificate{}, false return tls.Certificate{}, false
} }
keyPem, err := keyDatabase.Get(append(sni, '/', 'k', 'e', 'y'))
resGob := bytes.NewBuffer(resBytes)
resDec := gob.NewDecoder(resGob)
res := &certificate.Resource{}
err = resDec.Decode(res)
if err != nil { if err != nil {
// key database is not working or key doesn't exist
panic(err) panic(err)
} }
tlsCertificate, err := tls.X509KeyPair(certPem, keyPem) tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if !bytes.Equal(sni, MainDomainSuffix) {
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil {
panic(err)
}
// renew certificates 7 days before they expire
if !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(-7 * 24 * time.Hour)) {
go (func() {
tlsCertificate, err = obtainCert(acmeClient, []string{string(sni)}, res)
if err != nil {
log.Printf("Couldn't renew certificate for %s: %s", sni, err)
}
})()
}
}
return tlsCertificate, true return tlsCertificate, true
} }
var obtainLocks = sync.Map{} var obtainLocks = sync.Map{}
func obtainCert(acmeClient *lego.Client, domains []string) (tls.Certificate, error) { func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource) (tls.Certificate, error) {
name := strings.TrimPrefix(domains[0], "*") name := strings.TrimPrefix(domains[0], "*")
if os.Getenv("DNS_PROVIDER") == "" && len(domains[0]) > 0 && domains[0][0] == '*' { if os.Getenv("DNS_PROVIDER") == "" && len(domains[0]) > 0 && domains[0][0] == '*' {
domains = domains[1:] domains = domains[1:]
@ -251,24 +265,36 @@ func obtainCert(acmeClient *lego.Client, domains []string) (tls.Certificate, err
} }
defer obtainLocks.Delete(name) defer obtainLocks.Delete(name)
// request actual cert
var res *certificate.Resource
var err error
if renew != nil {
acmeClientRequestLimit.Take()
log.Printf("Renewing certificate for %v", domains)
res, err = acmeClient.Certificate.Renew(*renew, true, false, "")
} else {
acmeClientOrderLimit.Take()
acmeClientRequestLimit.Take()
log.Printf("Requesting new certificate for %v", domains) log.Printf("Requesting new certificate for %v", domains)
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{ res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{
Domains: domains, Domains: domains,
Bundle: true, Bundle: true,
MustStaple: true, MustStaple: false,
}) })
}
if err != nil { if err != nil {
log.Printf("Couldn't obtain certificate for %v: %s", domains, err) log.Printf("Couldn't obtain certificate for %v: %s", domains, err)
return tls.Certificate{}, err return tls.Certificate{}, err
} }
log.Printf("Obtained certificate for %v", domains) log.Printf("Obtained certificate for %v", domains)
err = keyDatabase.Put([]byte(name + "/key"), res.PrivateKey) var resGob bytes.Buffer
resEnc := gob.NewEncoder(&resGob)
err = resEnc.Encode(res)
if err != nil { if err != nil {
obtainLocks.Delete(name)
panic(err) panic(err)
} }
err = keyDatabase.Put([]byte(name), res.Certificate) err = keyDatabase.Put([]byte(name), resGob.Bytes())
if err != nil { if err != nil {
_ = keyDatabase.Delete([]byte(name + "/key")) _ = keyDatabase.Delete([]byte(name + "/key"))
obtainLocks.Delete(name) obtainLocks.Delete(name)
@ -307,7 +333,7 @@ func setupCertificates() {
panic(err) panic(err)
} }
myAcmeConfig = lego.NewConfig(&myAcmeAccount) myAcmeConfig = lego.NewConfig(&myAcmeAccount)
myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme.zerossl.com/v2/DV90") myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme-v02.api.letsencrypt.org/directory")
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
newAcmeClient(func(manager *resolver.SolverManager) error { return nil }) newAcmeClient(func(manager *resolver.SolverManager) error { return nil })
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
@ -321,7 +347,7 @@ func setupCertificates() {
KeyPEM: string(certcrypto.PEMEncode(privateKey)), KeyPEM: string(certcrypto.PEMEncode(privateKey)),
} }
myAcmeConfig = lego.NewConfig(&myAcmeAccount) myAcmeConfig = lego.NewConfig(&myAcmeAccount)
myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme.zerossl.com/v2/DV90") myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme-v02.api.letsencrypt.org/directory")
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
tempClient := newAcmeClient(func(manager *resolver.SolverManager) error { return nil }) tempClient := newAcmeClient(func(manager *resolver.SolverManager) error { return nil })
@ -371,6 +397,17 @@ func setupCertificates() {
return challenge.SetDNS01Provider(provider) return challenge.SetDNS01Provider(provider)
}) })
resBytes, err := keyDatabase.Get(MainDomainSuffix)
if err != nil {
// key database is not working
panic(err)
} else if resBytes == nil {
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])}, nil)
if err != nil {
log.Fatalf("Couldn't renew certificate for *%s: %s", MainDomainSuffix, err)
}
}
go (func() { go (func() {
for { for {
err := keyDatabase.Sync() err := keyDatabase.Sync()
@ -383,13 +420,20 @@ func setupCertificates() {
go (func() { go (func() {
for { for {
// clean up expired certs // clean up expired certs
keySuffix := []byte("/key")
now := time.Now() now := time.Now()
expiredCertCount := 0 expiredCertCount := 0
key, value, err := keyDatabase.Items().Next() key, resBytes, err := keyDatabase.Items().Next()
for err == nil { for err == nil {
if !bytes.HasSuffix(key, keySuffix) { if !bytes.Equal(key, MainDomainSuffix) {
tlsCertificates, err := certcrypto.ParsePEMBundle(value) resGob := bytes.NewBuffer(resBytes)
resDec := gob.NewDecoder(resGob)
res := &certificate.Resource{}
err = resDec.Decode(res)
if err != nil {
panic(err)
}
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
if err != nil || !tlsCertificates[0].NotAfter.After(now) { if err != nil || !tlsCertificates[0].NotAfter.After(now) {
err := keyDatabase.Delete(key) err := keyDatabase.Delete(key)
if err != nil { if err != nil {
@ -399,7 +443,7 @@ func setupCertificates() {
} }
} }
} }
key, value, err = keyDatabase.Items().Next() key, resBytes, err = keyDatabase.Items().Next()
} }
log.Printf("Removed %d expired certificates from the database", expiredCertCount) log.Printf("Removed %d expired certificates from the database", expiredCertCount)
@ -412,14 +456,30 @@ func setupCertificates() {
} }
// update main cert // update main cert
certPem, err := keyDatabase.Get(MainDomainSuffix) resBytes, err = keyDatabase.Get(MainDomainSuffix)
if err != nil { if err != nil {
// key database is not working // key database is not working
panic(err) panic(err)
} }
tlsCertificates, err := certcrypto.ParsePEMBundle(certPem)
if err != nil || !tlsCertificates[0].NotAfter.After(time.Now().Add(-48 * time.Hour)) { resGob := bytes.NewBuffer(resBytes)
_, _ = obtainCert(mainDomainAcmeClient, []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])}) resDec := gob.NewDecoder(resGob)
res := &certificate.Resource{}
err = resDec.Decode(res)
if err != nil {
panic(err)
}
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
// renew main certificate 30 days before it expires
if !tlsCertificates[0].NotAfter.After(time.Now().Add(-30 * 24 * time.Hour)) {
go (func() {
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])}, res)
if err != nil {
log.Printf("Couldn't renew certificate for *%s: %s", MainDomainSuffix, err)
}
})()
} }
time.Sleep(12 * time.Hour) time.Sleep(12 * time.Hour)