mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2024-11-18 10:29:43 +00:00
Switch back to Let's Encrypt again & implement renewal
This commit is contained in:
parent
77321eb181
commit
b19a5ecc1d
2 changed files with 105 additions and 44 deletions
|
@ -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.
|
||||||
|
|
146
certificates.go
146
certificates.go
|
@ -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)
|
||||||
|
|
||||||
log.Printf("Requesting new certificate for %v", domains)
|
// request actual cert
|
||||||
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{
|
var res *certificate.Resource
|
||||||
Domains: domains,
|
var err error
|
||||||
Bundle: true,
|
if renew != nil {
|
||||||
MustStaple: true,
|
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)
|
||||||
|
res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{
|
||||||
|
Domains: domains,
|
||||||
|
Bundle: 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)
|
||||||
|
|
Loading…
Reference in a new issue