Support custom ACME provider (#18340)

* Added ACMECAURL option to support custom ACME provider. Closes #18306
* Refactor setting.go https settings, renamed options and variables, and documented app.example.ini
* Refactored runLetsEncrypt to runACME
* Improved documentation
This commit is contained in:
Cristian Le 2022-02-08 14:45:35 +09:00 committed by GitHub
parent a60e8be8d1
commit 60f203385e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 51 deletions

View file

@ -222,10 +222,10 @@ func listen(m http.Handler, handleRedirector bool) error {
} }
err = runHTTP("tcp", listenAddr, "Web", m) err = runHTTP("tcp", listenAddr, "Web", m)
case setting.HTTPS: case setting.HTTPS:
if setting.EnableLetsEncrypt { if setting.EnableAcme {
err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, m) err = runACME(listenAddr, m)
break break
} } else {
if handleRedirector { if handleRedirector {
if setting.RedirectOtherPort { if setting.RedirectOtherPort {
go runHTTPRedirector() go runHTTPRedirector()
@ -234,6 +234,7 @@ func listen(m http.Handler, handleRedirector bool) error {
} }
} }
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m) err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m)
}
case setting.FCGI: case setting.FCGI:
if handleRedirector { if handleRedirector {
NoHTTPRedirector() NoHTTPRedirector()

View file

@ -5,7 +5,11 @@
package cmd package cmd
import ( import (
"crypto/x509"
"encoding/pem"
"fmt"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
@ -16,7 +20,25 @@ import (
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
) )
func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler) error { func getCARoot(path string) (*x509.CertPool, error) {
r, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(r)
if block == nil {
return nil, fmt.Errorf("no PEM found in the file %s", path)
}
caRoot, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
certPool.AddCert(caRoot)
return certPool, nil
}
func runACME(listenAddr string, m http.Handler) error {
// If HTTP Challenge enabled, needs to be serving on port 80. For TLSALPN needs 443. // If HTTP Challenge enabled, needs to be serving on port 80. For TLSALPN needs 443.
// Due to docker port mapping this can't be checked programmatically // Due to docker port mapping this can't be checked programmatically
// TODO: these are placeholders until we add options for each in settings with appropriate warning // TODO: these are placeholders until we add options for each in settings with appropriate warning
@ -33,10 +55,21 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
} }
magic := certmagic.NewDefault() magic := certmagic.NewDefault()
magic.Storage = &certmagic.FileStorage{Path: directory} magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
// Try to use private CA root if provided, otherwise defaults to system's trust
var certPool *x509.CertPool
if setting.AcmeCARoot != "" {
var err error
certPool, err = getCARoot(setting.AcmeCARoot)
if err != nil {
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
}
}
myACME := certmagic.NewACMEManager(magic, certmagic.ACMEManager{ myACME := certmagic.NewACMEManager(magic, certmagic.ACMEManager{
Email: email, CA: setting.AcmeURL,
Agreed: setting.LetsEncryptTOS, TrustedRoots: certPool,
Email: setting.AcmeEmail,
Agreed: setting.AcmeTOS,
DisableHTTPChallenge: !enableHTTPChallenge, DisableHTTPChallenge: !enableHTTPChallenge,
DisableTLSALPNChallenge: !enableTLSALPNChallenge, DisableTLSALPNChallenge: !enableTLSALPNChallenge,
ListenHost: setting.HTTPAddr, ListenHost: setting.HTTPAddr,
@ -47,7 +80,7 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
magic.Issuers = []certmagic.Issuer{myACME} magic.Issuers = []certmagic.Issuer{myACME}
// this obtains certificates or renews them if necessary // this obtains certificates or renews them if necessary
err := magic.ManageSync(graceful.GetManager().HammerContext(), []string{domain}) err := magic.ManageSync(graceful.GetManager().HammerContext(), []string{setting.Domain})
if err != nil { if err != nil {
return err return err
} }

View file

@ -178,6 +178,36 @@ RUN_MODE = ; prod
;OFFLINE_MODE = false ;OFFLINE_MODE = false
;DISABLE_ROUTER_LOG = false ;DISABLE_ROUTER_LOG = false
;; ;;
;; TLS Settings: Either ACME or manual
;; (Other common TLS configuration are found before)
;ENABLE_ACME = false
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; ACME automatic TLS settings
;;
;; ACME directory URL (e.g. LetsEncrypt's staging/testing URL: https://acme-staging-v02.api.letsencrypt.org/directory)
;; Leave empty to default to LetsEncrypt's (production) URL
;ACME_URL =
;;
;; Explicitly accept the ACME's TOS. The specific TOS cannot be retrieved at the moment.
;ACME_ACCEPTTOS = false
;;
;; If the ACME CA is not in your system's CA trust chain, it can be manually added here
;ACME_CA_ROOT =
;;
;; Email used for the ACME registration service
;; Can be left blank to initialize at first run and use the cached value
;ACME_EMAIL =
;;
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
;ACME_DIRECTORY = https
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Manual TLS settings: (Only applicable if ENABLE_ACME=false)
;;
;; Generate steps: ;; Generate steps:
;; $ ./gitea cert -ca=true -duration=8760h0m0s -host=myhost.example.com ;; $ ./gitea cert -ca=true -duration=8760h0m0s -host=myhost.example.com
;; ;;

View file

@ -292,8 +292,8 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `MINIMUM_KEY_SIZE_CHECK`: **true**: Indicate whether to check minimum key size with corresponding type. - `MINIMUM_KEY_SIZE_CHECK`: **true**: Indicate whether to check minimum key size with corresponding type.
- `OFFLINE_MODE`: **false**: Disables use of CDN for static files and Gravatar for profile pictures. - `OFFLINE_MODE`: **false**: Disables use of CDN for static files and Gravatar for profile pictures.
- `CERT_FILE`: **https/cert.pem**: Cert file path used for HTTPS. When chaining, the server certificate must come first, then intermediate CA certificates (if any). From 1.11 paths are relative to `CUSTOM_PATH`. - `CERT_FILE`: **https/cert.pem**: Cert file path used for HTTPS. When chaining, the server certificate must come first, then intermediate CA certificates (if any). This is ignored if `ENABLE_ACME=true`. From 1.11 paths are relative to `CUSTOM_PATH`.
- `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. From 1.11 paths are relative to `CUSTOM_PATH`. - `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. This is ignored if `ENABLE_ACME=true`. From 1.11 paths are relative to `CUSTOM_PATH`.
- `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path. - `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path.
- `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data. - `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data.
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev". - `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev".
@ -347,11 +347,12 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- Aliased names - Aliased names
- "ecdhe_rsa_with_chacha20_poly1305" is an alias for "ecdhe_rsa_with_chacha20_poly1305_sha256" - "ecdhe_rsa_with_chacha20_poly1305" is an alias for "ecdhe_rsa_with_chacha20_poly1305_sha256"
- "ecdhe_ecdsa_with_chacha20_poly1305" is alias for "ecdhe_ecdsa_with_chacha20_poly1305_sha256" - "ecdhe_ecdsa_with_chacha20_poly1305" is alias for "ecdhe_ecdsa_with_chacha20_poly1305_sha256"
- `ENABLE_LETSENCRYPT`: **false**: If enabled you must set `DOMAIN` to valid internet facing domain (ensure DNS is set and port 80 is accessible by letsencrypt validation server). - `ENABLE_ACME`: **false**: Flag to enable automatic certificate management via an ACME capable Certificate Authority (CA) server (default: Lets Encrypt). If enabled, `CERT_FILE` and `KEY_FILE` are ignored, and the CA must resolve `DOMAIN` to this gitea server. Ensure that DNS records are set and either port `80` or port `443` are accessible by the CA server (the public internet by default), and redirected to the appropriate ports `PORT_TO_REDIRECT` or `HTTP_PORT` respectively.
By using Lets Encrypt **you must consent** to their [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf). - `ACME_URL`: **\<empty\>**: The CA's ACME directory URL, e.g. for a self-hosted [smallstep CA server](https://github.com/smallstep/certificates), it can look like `https://ca.example.com/acme/acme/directory`. If left empty, it defaults to using Let's Encerypt's production CA (check `LETSENCRYPT_ACCEPTTOS` as well).
- `LETSENCRYPT_ACCEPTTOS`: **false**: This is an explicit check that you accept the terms of service for Let's Encrypt. - `ACME_ACCEPTTOS`: **false**: This is an explicit check that you accept the terms of service of the ACME provider. The default is Lets Encrypt [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf).
- `LETSENCRYPT_DIRECTORY`: **https**: Directory that Letsencrypt will use to cache information such as certs and private keys. - `ACME_DIRECTORY`: **https**: Directory that the certificate manager will use to cache information such as certs and private keys.
- `LETSENCRYPT_EMAIL`: **email@example.com**: Email used by Letsencrypt to notify about problems with issued certificates. (No default) - `ACME_EMAIL`: **\<empty\>**: Email used for the ACME registration. Usually it is to notify about problems with issued certificates.
- `ACME_CA_ROOT`: **\<empty\>**: The CA's root certificate. If left empty, it defaults to using the system's trust chain.
- `ALLOW_GRACEFUL_RESTARTS`: **true**: Perform a graceful restart on SIGHUP - `ALLOW_GRACEFUL_RESTARTS`: **true**: Perform a graceful restart on SIGHUP
- `GRACEFUL_HAMMER_TIME`: **60s**: After a restart the parent process will stop accepting new connections and will allow requests to finish before stopping. Shutdown will be forced if it takes longer than this time. - `GRACEFUL_HAMMER_TIME`: **60s**: After a restart the parent process will stop accepting new connections and will allow requests to finish before stopping. Shutdown will be forced if it takes longer than this time.
- `STARTUP_TIMEOUT`: **0**: Shutsdown the server if startup takes longer than the provided time. On Windows setting this sends a waithint to the SVC host to tell the SVC host startup may take some time. Please note startup is determined by the opening of the listeners - HTTP/HTTPS/SSH. Indexers may take longer to startup and can have their own timeouts. - `STARTUP_TIMEOUT`: **0**: Shutsdown the server if startup takes longer than the provided time. On Windows setting this sends a waithint to the SVC host to tell the SVC host startup may take some time. Please note startup is determined by the opening of the listeners - HTTP/HTTPS/SSH. Indexers may take longer to startup and can have their own timeouts.

View file

@ -55,20 +55,34 @@ PORT_TO_REDIRECT = 3080
If you are using Docker, make sure that this port is configured in your `docker-compose.yml` file. If you are using Docker, make sure that this port is configured in your `docker-compose.yml` file.
## Using Let's Encrypt ## Using ACME (Default: Let's Encrypt)
[Let's Encrypt](https://letsencrypt.org/) is a Certificate Authority that allows you to automatically request and renew SSL/TLS certificates. In addition to starting Gitea on your configured port, to request HTTPS certificates, Gitea will also need to listed on port 80, and will set up an autoredirect to HTTPS for you. Let's Encrypt will need to be able to access Gitea via the Internet to verify your ownership of the domain. [ACME](https://tools.ietf.org/html/rfc8555) is a Certificate Authority standard protocol that allows you to automatically request and renew SSL/TLS certificates. [Let's Encrypt](https://letsencrypt.org/) is a free publicly trusted Certificate Authority server using this standard. Only `HTTP-01` and `TLS-ALPN-01` challenges are implemented. In order for ACME challenges to pass and verify your domain ownership, external traffic to the gitea domain on port `80` (`HTTP-01`) or port `443` (`TLS-ALPN-01`) has to be served by the gitea instance. Setting up [HTTP redirection](#setting-up-http-redirection) and port-forwards might be needed for external traffic to route correctly. Normal traffic to port `80` will otherwise be automatically redirected to HTTPS. **You must consent** to the ACME provider's terms of service (default Let's Encrypt's [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)).
By using Let's Encrypt **you must consent** to their [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf).
Minimum setup using the default Let's Encrypt:
```ini ```ini
[server] [server]
PROTOCOL=https PROTOCOL=https
DOMAIN=git.example.com DOMAIN=git.example.com
ENABLE_LETSENCRYPT=true ENABLE_ACME=true
LETSENCRYPT_ACCEPTTOS=true ACME_ACCEPTTOS=true
LETSENCRYPT_DIRECTORY=https ACME_DIRECTORY=https
LETSENCRYPT_EMAIL=email@example.com ;; Email can be omitted here and provided manually at first run, after which it is cached
ACME_EMAIL=email@example.com
```
Minimumg setup using a [smallstep CA](https://github.com/smallstep/certificates), refer to [their tutorial](https://smallstep.com/docs/tutorials/acme-challenge) for more information.
```ini
[server]
PROTOCOL=https
DOMAIN=git.example.com
ENABLE_ACME=true
ACME_ACCEPTTOS=true
ACME_URL=https://ca.example.com/acme/acme/directory
;; Can be omitted if using the system's trust is preferred
;ACME_CA_ROOT=/path/to/root_ca.crt
ACME_DIRECTORY=https
ACME_EMAIL=email@example.com
``` ```
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server-server). To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server-server).

View file

@ -108,10 +108,12 @@ var (
UnixSocketPermission uint32 UnixSocketPermission uint32
EnablePprof bool EnablePprof bool
PprofDataPath string PprofDataPath string
EnableLetsEncrypt bool EnableAcme bool
LetsEncryptTOS bool AcmeTOS bool
LetsEncryptDirectory string AcmeLiveDirectory string
LetsEncryptEmail string AcmeEmail string
AcmeURL string
AcmeCARoot string
SSLMinimumVersion string SSLMinimumVersion string
SSLMaximumVersion string SSLMaximumVersion string
SSLCurvePreferences []string SSLCurvePreferences []string
@ -622,14 +624,54 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
switch protocolCfg { switch protocolCfg {
case "https": case "https":
Protocol = HTTPS Protocol = HTTPS
// FIXME: DEPRECATED to be removed in v1.18.0
if sec.HasKey("ENABLE_ACME") {
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
} else {
deprecatedSetting("server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME")
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
}
if EnableAcme {
AcmeURL = sec.Key("ACME_URL").MustString("")
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
// FIXME: DEPRECATED to be removed in v1.18.0
if sec.HasKey("ACME_ACCEPTTOS") {
AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false)
} else {
deprecatedSetting("server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS")
AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false)
}
if !AcmeTOS {
log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).")
}
// FIXME: DEPRECATED to be removed in v1.18.0
if sec.HasKey("ACME_DIRECTORY") {
AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https")
} else {
deprecatedSetting("server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY")
AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https")
}
// FIXME: DEPRECATED to be removed in v1.18.0
if sec.HasKey("ACME_EMAIL") {
AcmeEmail = sec.Key("ACME_EMAIL").MustString("")
} else {
deprecatedSetting("server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL")
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
}
} else {
CertFile = sec.Key("CERT_FILE").String() CertFile = sec.Key("CERT_FILE").String()
KeyFile = sec.Key("KEY_FILE").String() KeyFile = sec.Key("KEY_FILE").String()
if !filepath.IsAbs(CertFile) && len(CertFile) > 0 { if len(CertFile) > 0 && !filepath.IsAbs(CertFile) {
CertFile = filepath.Join(CustomPath, CertFile) CertFile = filepath.Join(CustomPath, CertFile)
} }
if !filepath.IsAbs(KeyFile) && len(KeyFile) > 0 { if len(KeyFile) > 0 && !filepath.IsAbs(KeyFile) {
KeyFile = filepath.Join(CustomPath, KeyFile) KeyFile = filepath.Join(CustomPath, KeyFile)
} }
}
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
SSLCipherSuites = sec.Key("SSL_CIPHER_SUITES").Strings(",")
case "fcgi": case "fcgi":
Protocol = FCGI Protocol = FCGI
case "fcgi+unix", "unix", "http+unix": case "fcgi+unix", "unix", "http+unix":
@ -653,18 +695,6 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
} }
} }
EnableLetsEncrypt = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
LetsEncryptTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false)
if !LetsEncryptTOS && EnableLetsEncrypt {
log.Warn("Failed to enable Let's Encrypt due to Let's Encrypt TOS not being accepted")
EnableLetsEncrypt = false
}
LetsEncryptDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https")
LetsEncryptEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
SSLCipherSuites = sec.Key("SSL_CIPHER_SUITES").Strings(",")
GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true) GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true)
GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second) GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second)
StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second) StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second)