mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2025-04-19 03:26:57 +00:00
Add option to disable DNS ACME provider (#290)
This PR add the `$NO_DNS_01` option (disabled by default) that removes the DNS ACME provider, and replaces the wildcard certificate by individual certificates obtained using the TLS ACME provider. This option allows an instance to work without having to manage access tokens for the DNS provider. On the flip side, this means that a certificate can be requested for each subdomains. To limit the risk of DOS, the existence of the user/org corresponding to a subdomain is checked before requesting a cert, however, this limitation is not enough for an forge with a high number of users/orgs. Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/290 Reviewed-by: Moritz Marquardt <momar@noreply.codeberg.org> Co-authored-by: Jean-Marie 'Histausse' Mineau <histausse@protonmail.com> Co-committed-by: Jean-Marie 'Histausse' Mineau <histausse@protonmail.com>
This commit is contained in:
parent
dd6d8bd60f
commit
03881382a4
12 changed files with 83 additions and 26 deletions
|
@ -13,8 +13,8 @@ var ErrAcmeMissConfig = errors.New("ACME client has wrong config")
|
|||
|
||||
func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) {
|
||||
// check config
|
||||
if (!cfg.AcceptTerms || cfg.DNSProvider == "") && cfg.APIEndpoint != "https://acme.mock.directory" {
|
||||
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
|
||||
if (!cfg.AcceptTerms || (cfg.DNSProvider == "" && !cfg.NoDNS01)) && cfg.APIEndpoint != "https://acme.mock.directory" {
|
||||
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER or $NO_DNS_01, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
|
||||
}
|
||||
if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" {
|
||||
return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig)
|
||||
|
|
|
@ -56,11 +56,8 @@ func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache
|
|||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if cfg.DNSProvider == "" {
|
||||
// using mock server, don't use wildcard certs
|
||||
err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
// using mock wildcard certs
|
||||
mainDomainAcmeClient = nil
|
||||
} else {
|
||||
// use DNS-Challenge https://go-acme.github.io/lego/dns/
|
||||
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
|
||||
|
|
|
@ -33,6 +33,8 @@ func TLSConfig(mainDomainSuffix string,
|
|||
firstDefaultBranch string,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.ICache,
|
||||
certDB database.CertDB,
|
||||
noDNS01 bool,
|
||||
rawDomain string,
|
||||
) *tls.Config {
|
||||
return &tls.Config{
|
||||
// check DNS name & get certificate from Let's Encrypt
|
||||
|
@ -64,9 +66,24 @@ func TLSConfig(mainDomainSuffix string,
|
|||
|
||||
targetOwner := ""
|
||||
mayObtainCert := true
|
||||
|
||||
if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
domain = mainDomainSuffix
|
||||
if noDNS01 {
|
||||
// Limit the domains allowed to request a certificate to pages-server domains
|
||||
// and domains for an existing user of org
|
||||
if !strings.EqualFold(domain, mainDomainSuffix[1:]) && !strings.EqualFold(domain, rawDomain) {
|
||||
targetOwner := strings.TrimSuffix(domain, mainDomainSuffix)
|
||||
owner_exist, err := giteaClient.GiteaCheckIfOwnerExists(targetOwner)
|
||||
mayObtainCert = owner_exist
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to check '%s' existence on the forge: %s", targetOwner, err)
|
||||
mayObtainCert = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
domain = mainDomainSuffix
|
||||
}
|
||||
} else {
|
||||
var targetRepo, targetBranch string
|
||||
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
|
@ -199,9 +216,6 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
|
|||
|
||||
func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
|
||||
name := strings.TrimPrefix(domains[0], "*")
|
||||
if useDnsProvider && domains[0] != "" && domains[0][0] == '*' {
|
||||
domains = domains[1:]
|
||||
}
|
||||
|
||||
// lock to avoid simultaneous requests
|
||||
_, working := c.obtainLocks.LoadOrStore(name, struct{}{})
|
||||
|
@ -219,7 +233,11 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew
|
|||
defer c.obtainLocks.Delete(name)
|
||||
|
||||
if acmeClient == nil {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
|
||||
if useDnsProvider {
|
||||
return mockCert(domains[0], "DNS ACME client is not defined", mainDomainSuffix, keyDatabase)
|
||||
} else {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
// request actual cert
|
||||
|
|
|
@ -52,7 +52,6 @@ func (x xDB) Close() error {
|
|||
func (x xDB) Put(domain string, cert *certificate.Resource) error {
|
||||
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db")
|
||||
|
||||
domain = integrationTestReplacements(domain)
|
||||
c, err := toCert(domain, cert)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -82,7 +81,6 @@ func (x xDB) Get(domain string) (*certificate.Resource, error) {
|
|||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
cert := new(Cert)
|
||||
log.Trace().Str("domain", domain).Msg("get cert from db")
|
||||
|
@ -99,7 +97,6 @@ func (x xDB) Delete(domain string) error {
|
|||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
log.Trace().Str("domain", domain).Msg("delete cert from db")
|
||||
_, err := x.engine.ID(domain).Delete(new(Cert))
|
||||
|
@ -139,13 +136,3 @@ func supportedDriver(driver string) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// integrationTestReplacements is needed because integration tests use a single domain cert,
|
||||
// while production use a wildcard cert
|
||||
// TODO: find a better way to handle this
|
||||
func integrationTestReplacements(domainKey string) string {
|
||||
if domainKey == "*.localhost.mock.directory" {
|
||||
return "localhost.mock.directory"
|
||||
}
|
||||
return domainKey
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ const (
|
|||
// TODO: move as option into cache interface
|
||||
fileCacheTimeout = 5 * time.Minute
|
||||
|
||||
// ownerExistenceCacheTimeout specifies the timeout for the existence of a repo/org
|
||||
ownerExistenceCacheTimeout = 5 * time.Minute
|
||||
|
||||
// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
|
||||
fileCacheSizeLimit = int64(1000 * 1000)
|
||||
)
|
||||
|
|
|
@ -28,6 +28,7 @@ const (
|
|||
branchTimestampCacheKeyPrefix = "branchTime"
|
||||
defaultBranchCacheKeyPrefix = "defaultBranch"
|
||||
rawContentCacheKeyPrefix = "rawContent"
|
||||
ownerExistenceKeyPrefix = "ownerExist"
|
||||
|
||||
// pages server
|
||||
PagesCacheIndicatorHeader = "X-Pages-Cache"
|
||||
|
@ -266,6 +267,38 @@ func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (str
|
|||
return branch, nil
|
||||
}
|
||||
|
||||
func (client *Client) GiteaCheckIfOwnerExists(owner string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s", ownerExistenceKeyPrefix, owner)
|
||||
|
||||
if exist, ok := client.responseCache.Get(cacheKey); ok && exist != nil {
|
||||
return exist.(bool), nil
|
||||
}
|
||||
|
||||
_, resp, err := client.sdkClient.GetUserInfo(owner)
|
||||
if resp.StatusCode == http.StatusOK && err == nil {
|
||||
if err := client.responseCache.Set(cacheKey, true, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return true, nil
|
||||
} else if resp.StatusCode != http.StatusNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, resp, err = client.sdkClient.GetOrg(owner)
|
||||
if resp.StatusCode == http.StatusOK && err == nil {
|
||||
if err := client.responseCache.Set(cacheKey, true, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return true, nil
|
||||
} else if resp.StatusCode != http.StatusNotFound {
|
||||
return false, err
|
||||
}
|
||||
if err := client.responseCache.Set(cacheKey, false, ownerExistenceCacheTimeout); err != nil {
|
||||
log.Error().Err(err).Msg("[cache] error on cache write")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (client *Client) getMimeTypeByExtension(resource string) string {
|
||||
mimeType := mime.TypeByExtension(path.Ext(resource))
|
||||
mimeTypeSplit := strings.SplitN(mimeType, ";", 2)
|
||||
|
|
|
@ -110,6 +110,8 @@ func Serve(ctx *cli.Context) error {
|
|||
cfg.Server.PagesBranches[0],
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache,
|
||||
certDB,
|
||||
cfg.ACME.NoDNS01,
|
||||
cfg.Server.RawDomain,
|
||||
))
|
||||
|
||||
interval := 12 * time.Hour
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue