mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2024-11-18 10:29:43 +00:00
Make ACME account persistent & fix issues with certificate resolution
This commit is contained in:
parent
fcccd6435a
commit
77321eb181
3 changed files with 150 additions and 79 deletions
221
certificates.go
221
certificates.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/OrlovEvgeny/go-mcache"
|
"github.com/OrlovEvgeny/go-mcache"
|
||||||
"github.com/akrylysov/pogreb/fs"
|
"github.com/akrylysov/pogreb/fs"
|
||||||
|
@ -16,9 +17,11 @@ import (
|
||||||
"github.com/go-acme/lego/v4/challenge/resolver"
|
"github.com/go-acme/lego/v4/challenge/resolver"
|
||||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||||
"github.com/go-acme/lego/v4/providers/dns"
|
"github.com/go-acme/lego/v4/providers/dns"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/akrylysov/pogreb"
|
"github.com/akrylysov/pogreb"
|
||||||
|
@ -83,38 +86,30 @@ var tlsConfig = &tls.Config{
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsCertificate tls.Certificate
|
var tlsCertificate tls.Certificate
|
||||||
if ok, err := keyDatabase.Has(sniBytes); err != nil {
|
var err error
|
||||||
// key database is not working
|
var ok bool
|
||||||
panic(err)
|
if tlsCertificate, ok = retrieveCertFromDB(sniBytes); ok {
|
||||||
} else if ok {
|
|
||||||
// parse certificate from database
|
|
||||||
certPem, err := keyDatabase.Get(sniBytes)
|
|
||||||
if err != nil {
|
|
||||||
// key database is not working
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
keyPem, err := keyDatabase.Get(append(sniBytes, '/', 'k', 'e', 'y'))
|
|
||||||
if err != nil {
|
|
||||||
// key database is not working or key doesn't exist
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsCertificate, err = tls.X509KeyPair(certPem, keyPem)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
|
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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(-24 * time.Hour)) {
|
})()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := CheckUserLimit(targetOwner)
|
err = CheckUserLimit(targetOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -125,7 +120,7 @@ var tlsConfig = &tls.Config{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := keyCache.Set(sni, &tlsCertificate, 15 * time.Minute)
|
err = keyCache.Set(sni, &tlsCertificate, 15 * time.Minute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -133,6 +128,7 @@ var tlsConfig = &tls.Config{
|
||||||
},
|
},
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
NextProtos: []string{
|
NextProtos: []string{
|
||||||
|
"http/1.1",
|
||||||
tlsalpn01.ACMETLS1Protocol,
|
tlsalpn01.ACMETLS1Protocol,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -166,11 +162,14 @@ func CheckUserLimit(user string) (error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var myAcmeAccount AcmeAccount
|
||||||
|
var myAcmeConfig *lego.Config
|
||||||
|
|
||||||
type AcmeAccount struct {
|
type AcmeAccount struct {
|
||||||
Email string
|
Email string
|
||||||
Registration *registration.Resource
|
Registration *registration.Resource
|
||||||
key crypto.PrivateKey
|
Key crypto.PrivateKey `json:"-"`
|
||||||
limit equalizer.Limiter
|
KeyPEM string `json:"Key"`
|
||||||
}
|
}
|
||||||
func (u *AcmeAccount) GetEmail() string {
|
func (u *AcmeAccount) GetEmail() string {
|
||||||
return u.Email
|
return u.Email
|
||||||
|
@ -179,22 +178,11 @@ func (u AcmeAccount) GetRegistration() *registration.Resource {
|
||||||
return u.Registration
|
return u.Registration
|
||||||
}
|
}
|
||||||
func (u *AcmeAccount) GetPrivateKey() crypto.PrivateKey {
|
func (u *AcmeAccount) GetPrivateKey() crypto.PrivateKey {
|
||||||
return u.key
|
return u.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAcmeClient(configureChallenge func(*resolver.SolverManager) error) *lego.Client {
|
func newAcmeClient(configureChallenge func(*resolver.SolverManager) error) *lego.Client {
|
||||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
acmeClient, err := lego.NewClient(myAcmeConfig)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
myUser := AcmeAccount{
|
|
||||||
Email: envOr("ACME_EMAIL", "noreply@example.email"),
|
|
||||||
key: privateKey,
|
|
||||||
}
|
|
||||||
config := lego.NewConfig(&myUser)
|
|
||||||
config.CADirURL = envOr("ACME_API", "https://acme.zerossl.com/v2/DV90")
|
|
||||||
config.Certificate.KeyType = certcrypto.RSA2048
|
|
||||||
acmeClient, err := lego.NewClient(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -202,46 +190,12 @@ func newAcmeClient(configureChallenge func(*resolver.SolverManager) error) *lego
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// accept terms
|
|
||||||
if os.Getenv("ACME_EAB_KID") == "" || os.Getenv("ACME_EAB_HMAC") == "" {
|
|
||||||
reg, err := acmeClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: os.Getenv("ACME_ACCEPT_TERMS") == "true"})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
} else {
|
|
||||||
reg, err := acmeClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
|
||||||
TermsOfServiceAgreed: os.Getenv("ACME_ACCEPT_TERMS") == "true",
|
|
||||||
Kid: os.Getenv("ACME_EAB_KID"),
|
|
||||||
HmacEncoded: os.Getenv("ACME_EAB_HMAC"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
}
|
|
||||||
|
|
||||||
return acmeClient
|
return acmeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
var acmeClient = newAcmeClient(func(challenge *resolver.SolverManager) error {
|
var acmeClient, mainDomainAcmeClient *lego.Client
|
||||||
return challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{})
|
|
||||||
})
|
|
||||||
var acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
|
var acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
|
||||||
|
|
||||||
var mainDomainAcmeClient = newAcmeClient(func(challenge *resolver.SolverManager) error {
|
|
||||||
if os.Getenv("DNS_PROVIDER") == "" {
|
|
||||||
// using mock server, don't use wildcard certs
|
|
||||||
return challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{})
|
|
||||||
}
|
|
||||||
provider, err := dns.NewDNSChallengeProviderByName(os.Getenv("DNS_PROVIDER"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return challenge.SetDNS01Provider(provider)
|
|
||||||
})
|
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -252,12 +206,51 @@ func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func retrieveCertFromDB(sni []byte) (tls.Certificate, bool) {
|
||||||
|
// parse certificate from database
|
||||||
|
certPem, err := keyDatabase.Get(sni)
|
||||||
|
if err != nil {
|
||||||
|
// key database is not working
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if certPem == nil {
|
||||||
|
return tls.Certificate{}, false
|
||||||
|
}
|
||||||
|
keyPem, err := keyDatabase.Get(append(sni, '/', 'k', 'e', 'y'))
|
||||||
|
if err != nil {
|
||||||
|
// key database is not working or key doesn't exist
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCertificate, err := tls.X509KeyPair(certPem, keyPem)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return tlsCertificate, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var obtainLocks = sync.Map{}
|
||||||
func obtainCert(acmeClient *lego.Client, domains []string) (tls.Certificate, error) {
|
func obtainCert(acmeClient *lego.Client, domains []string) (tls.Certificate, error) {
|
||||||
name := 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:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lock to avoid simultaneous requests
|
||||||
|
_, working := obtainLocks.LoadOrStore(name, struct{}{})
|
||||||
|
if working {
|
||||||
|
for working {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
_, working = obtainLocks.Load(name)
|
||||||
|
}
|
||||||
|
cert, ok := retrieveCertFromDB([]byte(name))
|
||||||
|
if !ok {
|
||||||
|
return tls.Certificate{}, errors.New("certificate failed in synchronous request")
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
defer obtainLocks.Delete(name)
|
||||||
|
|
||||||
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,
|
||||||
|
@ -272,22 +265,24 @@ func obtainCert(acmeClient *lego.Client, domains []string) (tls.Certificate, err
|
||||||
|
|
||||||
err = keyDatabase.Put([]byte(name + "/key"), res.PrivateKey)
|
err = keyDatabase.Put([]byte(name + "/key"), res.PrivateKey)
|
||||||
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), res.Certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = keyDatabase.Delete([]byte(name + "/key"))
|
_ = keyDatabase.Delete([]byte(name + "/key"))
|
||||||
|
obtainLocks.Delete(name)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return tls.Certificate{}, err
|
||||||
}
|
}
|
||||||
return tlsCertificate, nil
|
return tlsCertificate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func setupCertificates() {
|
||||||
var err error
|
var err error
|
||||||
keyDatabase, err = pogreb.Open("key-database.pogreb", &pogreb.Options{
|
keyDatabase, err = pogreb.Open("key-database.pogreb", &pogreb.Options{
|
||||||
BackgroundSyncInterval: 30 * time.Second,
|
BackgroundSyncInterval: 30 * time.Second,
|
||||||
|
@ -302,6 +297,80 @@ func init() {
|
||||||
panic(errors.New("you must set ACME_ACCEPT_TERMS and DNS_PROVIDER, unless ACME_API is set to https://acme.mock.directory"))
|
panic(errors.New("you must set ACME_ACCEPT_TERMS and DNS_PROVIDER, unless ACME_API is set to https://acme.mock.directory"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if account, err := ioutil.ReadFile("acme-account.json"); err == nil {
|
||||||
|
err = json.Unmarshal(account, &myAcmeAccount)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||||
|
myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme.zerossl.com/v2/DV90")
|
||||||
|
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
newAcmeClient(func(manager *resolver.SolverManager) error { return nil })
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
myAcmeAccount = AcmeAccount{
|
||||||
|
Email: envOr("ACME_EMAIL", "noreply@example.email"),
|
||||||
|
Key: privateKey,
|
||||||
|
KeyPEM: string(certcrypto.PEMEncode(privateKey)),
|
||||||
|
}
|
||||||
|
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||||
|
myAcmeConfig.CADirURL = envOr("ACME_API", "https://acme.zerossl.com/v2/DV90")
|
||||||
|
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
tempClient := newAcmeClient(func(manager *resolver.SolverManager) error { return nil })
|
||||||
|
|
||||||
|
// accept terms & log in to EAB
|
||||||
|
if os.Getenv("ACME_EAB_KID") == "" || os.Getenv("ACME_EAB_HMAC") == "" {
|
||||||
|
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: os.Getenv("ACME_ACCEPT_TERMS") == "true"})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
myAcmeAccount.Registration = reg
|
||||||
|
} else {
|
||||||
|
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||||
|
TermsOfServiceAgreed: os.Getenv("ACME_ACCEPT_TERMS") == "true",
|
||||||
|
Kid: os.Getenv("ACME_EAB_KID"),
|
||||||
|
HmacEncoded: os.Getenv("ACME_EAB_HMAC"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
myAcmeAccount.Registration = reg
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeAccountJson, err := json.Marshal(myAcmeAccount)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile("acme-account.json", acmeAccountJson, 0600)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeClient = newAcmeClient(func(challenge *resolver.SolverManager) error {
|
||||||
|
return challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{})
|
||||||
|
})
|
||||||
|
mainDomainAcmeClient = newAcmeClient(func(challenge *resolver.SolverManager) error {
|
||||||
|
if os.Getenv("DNS_PROVIDER") == "" {
|
||||||
|
// using mock server, don't use wildcard certs
|
||||||
|
return challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{})
|
||||||
|
}
|
||||||
|
provider, err := dns.NewDNSChallengeProviderByName(os.Getenv("DNS_PROVIDER"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return challenge.SetDNS01Provider(provider)
|
||||||
|
})
|
||||||
|
|
||||||
go (func() {
|
go (func() {
|
||||||
for {
|
for {
|
||||||
err := keyDatabase.Sync()
|
err := keyDatabase.Sync()
|
||||||
|
|
|
@ -46,10 +46,10 @@ func getTargetFromDNS(domain string) (targetOwner, targetRepo, targetBranch stri
|
||||||
cnameParts := strings.Split(strings.TrimSuffix(cname, string(MainDomainSuffix)), ".")
|
cnameParts := strings.Split(strings.TrimSuffix(cname, string(MainDomainSuffix)), ".")
|
||||||
targetOwner = cnameParts[len(cnameParts)-1]
|
targetOwner = cnameParts[len(cnameParts)-1]
|
||||||
if len(cnameParts) > 1 {
|
if len(cnameParts) > 1 {
|
||||||
targetRepo = cnameParts[len(cnameParts)-1]
|
targetRepo = cnameParts[len(cnameParts)-2]
|
||||||
}
|
}
|
||||||
if len(cnameParts) > 2 {
|
if len(cnameParts) > 2 {
|
||||||
targetBranch = cnameParts[len(cnameParts)-2]
|
targetBranch = cnameParts[len(cnameParts)-3]
|
||||||
}
|
}
|
||||||
if targetRepo == "" {
|
if targetRepo == "" {
|
||||||
targetRepo = "pages"
|
targetRepo = "pages"
|
||||||
|
|
2
main.go
2
main.go
|
@ -102,6 +102,8 @@ func main() {
|
||||||
}
|
}
|
||||||
listener = tls.NewListener(listener, tlsConfig)
|
listener = tls.NewListener(listener, tlsConfig)
|
||||||
|
|
||||||
|
setupCertificates()
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
err = server.Serve(listener)
|
err = server.Serve(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue