feat: implement mutual tls in the web server (#3065)
Mutual TLS helps prevent untrusted clients communicating with services like Authelia. This can be utilized to reduce the attack surface. Fixes #3041pull/3113/head
parent
a2eb0316c8
commit
3ca438e3d5
|
@ -19,6 +19,9 @@ type HostEntry struct {
|
|||
}
|
||||
|
||||
var hostEntries = []HostEntry{
|
||||
// For unit tests.
|
||||
{Domain: "local.example.com", IP: "127.0.0.1"},
|
||||
|
||||
// For authelia backend.
|
||||
{Domain: "authelia.example.com", IP: "192.168.240.50"},
|
||||
|
||||
|
|
|
@ -71,6 +71,9 @@ server:
|
|||
## The path to the DER base64/PEM format public certificate.
|
||||
certificate: ""
|
||||
|
||||
## The list of certificates for client authentication.
|
||||
client_certificates: []
|
||||
|
||||
## Server headers configuration/customization.
|
||||
headers:
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ server:
|
|||
tls:
|
||||
key: ""
|
||||
certificate: ""
|
||||
client_certificates: []
|
||||
headers:
|
||||
csp_template: ""
|
||||
```
|
||||
|
@ -213,6 +214,19 @@ required: situational
|
|||
|
||||
The path to the public certificate for TLS connections. Must be in DER base64/PEM format.
|
||||
|
||||
#### client_certificates
|
||||
<div markdown="1">
|
||||
type: list(string)
|
||||
{: .label .label-config .label-purple }
|
||||
default: []
|
||||
{: .label .label-config .label-blue }
|
||||
required: no
|
||||
{: .label .label-config .label-yellow }
|
||||
</div>
|
||||
|
||||
The list of file paths to certificates used for authenticating clients. Those certificates can be root
|
||||
or intermediate certificates. If no item is provided mutual TLS is disabled.
|
||||
|
||||
|
||||
### headers
|
||||
|
||||
|
|
|
@ -189,6 +189,19 @@ Authelia protects your users against open redirect attacks by always checking if
|
|||
to a subdomain of the domain protected by Authelia. This prevents phishing campaigns tricking users into visiting
|
||||
infected websites leveraging legit links.
|
||||
|
||||
## Mutual TLS
|
||||
|
||||
For the best security protection, configuration with TLS is highly recommended. TLS is used to secure the connection between
|
||||
the proxies and Authelia instances meaning that an attacker on the network cannot perform a man-in-the-middle attack on those
|
||||
connections. However, an attacker on the network can still impersonate proxies but this can be prevented by configuring mutual
|
||||
TLS.
|
||||
Mutual TLS brings mutual authentication between Authelia and the proxies. Any other party attempting to contact Authelia
|
||||
would not even be able to create a TCP connection. This measure is recommended in all cases except if you already configured
|
||||
some kind of ACLs specifically allowing the communication between proxies and Authelia instances like in a service mesh or
|
||||
some kind of network overlay.
|
||||
|
||||
To configure mutual TLS, please refer to [this document](../configuration/server.md#client_certificates)
|
||||
|
||||
## Additional security
|
||||
|
||||
### Reset Password
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -113,14 +106,13 @@ func cmdCertificatesGenerateRun(cmd *cobra.Command, _ []string) {
|
|||
}
|
||||
|
||||
func cmdCertificatesGenerateRunExtended(hosts []string, ecdsaCurve, validFrom, certificateTargetDirectory string, ed25519Key, isCA bool, rsaBits int, validFor time.Duration) {
|
||||
priv, err := getPrivateKey(ecdsaCurve, ed25519Key, rsaBits)
|
||||
certPath := filepath.Join(certificateTargetDirectory, "cert.pem")
|
||||
keyPath := filepath.Join(certificateTargetDirectory, "key.pem")
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate private key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var notBefore time.Time
|
||||
var (
|
||||
notBefore time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
switch len(validFrom) {
|
||||
case 0:
|
||||
|
@ -128,122 +120,47 @@ func cmdCertificatesGenerateRunExtended(hosts []string, ecdsaCurve, validFrom, c
|
|||
default:
|
||||
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse start date: %v\n", err)
|
||||
os.Exit(1)
|
||||
log.Fatalf("Failed to parse start date: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
notAfter := notBefore.Add(validFor)
|
||||
var privateKeyBuilder utils.PrivateKeyBuilder
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate serial number: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, h)
|
||||
}
|
||||
}
|
||||
|
||||
if isCA {
|
||||
template.IsCA = true
|
||||
template.KeyUsage |= x509.KeyUsageCertSign
|
||||
}
|
||||
|
||||
certPath := filepath.Join(certificateTargetDirectory, "cert.pem")
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create certificate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
writePEM(derBytes, "CERTIFICATE", certPath)
|
||||
|
||||
fmt.Printf("Certificate Public Key written to %s\n", certPath)
|
||||
|
||||
keyPath := filepath.Join(certificateTargetDirectory, "key.pem")
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to marshal private key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
writePEM(privBytes, "PRIVATE KEY", keyPath)
|
||||
|
||||
fmt.Printf("Certificate Private Key written to %s\n", keyPath)
|
||||
}
|
||||
|
||||
func getPrivateKey(ecdsaCurve string, ed25519Key bool, rsaBits int) (priv interface{}, err error) {
|
||||
switch ecdsaCurve {
|
||||
case "":
|
||||
if ed25519Key {
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
privateKeyBuilder = utils.Ed25519KeyBuilder{}
|
||||
} else {
|
||||
priv, err = rsa.GenerateKey(rand.Reader, rsaBits)
|
||||
privateKeyBuilder = utils.RSAKeyBuilder{}.WithKeySize(rsaBits)
|
||||
}
|
||||
case "P224":
|
||||
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||||
privateKeyBuilder = utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P224())
|
||||
case "P256":
|
||||
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
privateKeyBuilder = utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
case "P384":
|
||||
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
privateKeyBuilder = utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P384())
|
||||
case "P521":
|
||||
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
privateKeyBuilder = utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P521())
|
||||
default:
|
||||
err = fmt.Errorf("unrecognized elliptic curve: %q", ecdsaCurve)
|
||||
log.Fatalf("Failed to generate private key: unrecognized elliptic curve: \"%s\"", ecdsaCurve)
|
||||
}
|
||||
|
||||
return priv, err
|
||||
}
|
||||
|
||||
func writePEM(bytes []byte, blockType, path string) {
|
||||
keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
certBytes, keyBytes, err := utils.GenerateCertificate(privateKeyBuilder, hosts, notBefore, validFor, isCA)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open %s for writing: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: blockType, Bytes: bytes}); err != nil {
|
||||
fmt.Printf("Failed to write data to %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
err = os.WriteFile(certPath, certBytes, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write %s for writing: %v", certPath, err)
|
||||
}
|
||||
|
||||
if err := keyOut.Close(); err != nil {
|
||||
fmt.Printf("Error closing %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) interface{} {
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
case ed25519.PrivateKey:
|
||||
return k.Public().(ed25519.PublicKey)
|
||||
default:
|
||||
return nil
|
||||
fmt.Printf("Certificate written to %s\n", certPath)
|
||||
|
||||
err = os.WriteFile(keyPath, keyBytes, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write %s for writing: %v", certPath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Private Key written to %s\n", keyPath)
|
||||
}
|
||||
|
|
|
@ -77,7 +77,9 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
|
|||
|
||||
doStartupChecks(config, &providers)
|
||||
|
||||
server.Start(*config, providers)
|
||||
s, listener := server.CreateServer(*config, providers)
|
||||
|
||||
logger.Fatal(s.Serve(listener))
|
||||
}
|
||||
|
||||
func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers) {
|
||||
|
|
|
@ -71,6 +71,9 @@ server:
|
|||
## The path to the DER base64/PEM format public certificate.
|
||||
certificate: ""
|
||||
|
||||
## The list of certificates for client authentication.
|
||||
client_certificates: []
|
||||
|
||||
## Server headers configuration/customization.
|
||||
headers:
|
||||
|
||||
|
|
|
@ -18,8 +18,9 @@ type ServerConfiguration struct {
|
|||
|
||||
// ServerTLSConfiguration represents the configuration of the http servers TLS options.
|
||||
type ServerTLSConfiguration struct {
|
||||
Certificate string `koanf:"certificate"`
|
||||
Key string `koanf:"key"`
|
||||
Certificate string `koanf:"certificate"`
|
||||
Key string `koanf:"key"`
|
||||
ClientCertificates []string `koanf:"client_certificates"`
|
||||
}
|
||||
|
||||
// ServerHeadersConfiguration represents the customization of the http server headers.
|
||||
|
|
|
@ -44,8 +44,6 @@ const (
|
|||
testLDAPURL = "ldap://ldap"
|
||||
testLDAPUser = "user"
|
||||
testModeDisabled = "disable"
|
||||
testTLSCert = "/tmp/cert.pem"
|
||||
testTLSKey = "/tmp/key.pem"
|
||||
testEncryptionKey = "a_not_so_secure_encryption_key"
|
||||
)
|
||||
|
||||
|
@ -222,8 +220,12 @@ const (
|
|||
|
||||
// Server Error constants.
|
||||
const (
|
||||
errFmtServerTLSCert = "server: tls: option 'key' must also be accompanied by option 'certificate'"
|
||||
errFmtServerTLSKey = "server: tls: option 'certificate' must also be accompanied by option 'key'"
|
||||
errFmtServerTLSCert = "server: tls: option 'key' must also be accompanied by option 'certificate'"
|
||||
errFmtServerTLSKey = "server: tls: option 'certificate' must also be accompanied by option 'key'"
|
||||
errFmtServerTLSCertFileDoesNotExist = "server: tls: file path %s provided in 'certificate' does not exist"
|
||||
errFmtServerTLSKeyFileDoesNotExist = "server: tls: file path %s provided in 'key' does not exist"
|
||||
errFmtServerTLSClientAuthCertFileDoesNotExist = "server: tls: client_certificates: certificates: file path %s does not exist"
|
||||
errFmtServerTLSClientAuthNoAuth = "server: tls: client authentication cannot be configured if no server certificate and key are provided"
|
||||
|
||||
errFmtServerPathNoForwardSlashes = "server: option 'path' must not contain any forward slashes"
|
||||
errFmtServerPathAlphaNum = "server: option 'path' must only contain alpha numeric characters"
|
||||
|
|
|
@ -9,6 +9,44 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// validateFileExists checks whether a file exist.
|
||||
func validateFileExists(path string, validator *schema.StructValidator, errTemplate string) {
|
||||
exist, err := utils.FileExists(path)
|
||||
if err != nil {
|
||||
validator.Push(fmt.Errorf("tls: unable to check if file %s exists: %s", path, err))
|
||||
}
|
||||
|
||||
if !exist {
|
||||
validator.Push(fmt.Errorf(errTemplate, path))
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateServerTLS checks a server TLS configuration is correct.
|
||||
func ValidateServerTLS(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
if config.Server.TLS.Key != "" && config.Server.TLS.Certificate == "" {
|
||||
validator.Push(fmt.Errorf(errFmtServerTLSCert))
|
||||
} else if config.Server.TLS.Key == "" && config.Server.TLS.Certificate != "" {
|
||||
validator.Push(fmt.Errorf(errFmtServerTLSKey))
|
||||
}
|
||||
|
||||
if config.Server.TLS.Key != "" {
|
||||
validateFileExists(config.Server.TLS.Key, validator, errFmtServerTLSKeyFileDoesNotExist)
|
||||
}
|
||||
|
||||
if config.Server.TLS.Certificate != "" {
|
||||
validateFileExists(config.Server.TLS.Certificate, validator, errFmtServerTLSCertFileDoesNotExist)
|
||||
}
|
||||
|
||||
if config.Server.TLS.Key == "" && config.Server.TLS.Certificate == "" &&
|
||||
len(config.Server.TLS.ClientCertificates) > 0 {
|
||||
validator.Push(fmt.Errorf(errFmtServerTLSClientAuthNoAuth))
|
||||
}
|
||||
|
||||
for _, clientCertPath := range config.Server.TLS.ClientCertificates {
|
||||
validateFileExists(clientCertPath, validator, errFmtServerTLSClientAuthCertFileDoesNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateServer checks a server configuration is correct.
|
||||
func ValidateServer(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
if config.Server.Host == "" {
|
||||
|
@ -19,11 +57,7 @@ func ValidateServer(config *schema.Configuration, validator *schema.StructValida
|
|||
config.Server.Port = schema.DefaultServerConfiguration.Port
|
||||
}
|
||||
|
||||
if config.Server.TLS.Key != "" && config.Server.TLS.Certificate == "" {
|
||||
validator.Push(fmt.Errorf(errFmtServerTLSCert))
|
||||
} else if config.Server.TLS.Key == "" && config.Server.TLS.Certificate != "" {
|
||||
validator.Push(fmt.Errorf(errFmtServerTLSKey))
|
||||
}
|
||||
ValidateServerTLS(config, validator)
|
||||
|
||||
switch {
|
||||
case strings.Contains(config.Server.Path, "/"):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -9,6 +10,8 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
const unexistingFilePath = "/tmp/unexisting_file"
|
||||
|
||||
func TestShouldSetDefaultServerValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.Configuration{}
|
||||
|
@ -119,33 +122,129 @@ func TestShouldValidateAndUpdateHost(t *testing.T) {
|
|||
func TestShouldRaiseErrorWhenTLSCertWithoutKeyIsProvided(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
config.Server.TLS.Certificate = testTLSCert
|
||||
|
||||
file, err := os.CreateTemp("", "cert")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
config.Server.TLS.Certificate = file.Name()
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: option 'certificate' must also be accompanied by option 'key'")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenTLSCertDoesNotExist(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
file, err := os.CreateTemp("", "key")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
config.Server.TLS.Certificate = unexistingFilePath
|
||||
config.Server.TLS.Key = file.Name()
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: file path /tmp/unexisting_file provided in 'certificate' does not exist")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
config.Server.TLS.Key = testTLSKey
|
||||
|
||||
file, err := os.CreateTemp("", "key")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
config.Server.TLS.Key = file.Name()
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: option 'key' must also be accompanied by option 'certificate'")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenTLSKeyDoesNotExist(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
file, err := os.CreateTemp("", "key")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
config.Server.TLS.Key = unexistingFilePath
|
||||
config.Server.TLS.Certificate = file.Name()
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: file path /tmp/unexisting_file provided in 'key' does not exist")
|
||||
}
|
||||
|
||||
func TestShouldNotRaiseErrorWhenBothTLSCertificateAndKeyAreProvided(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
config.Server.TLS.Certificate = testTLSCert
|
||||
config.Server.TLS.Key = testTLSKey
|
||||
|
||||
certFile, err := os.CreateTemp("", "cert")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(certFile.Name())
|
||||
|
||||
keyFile, err := os.CreateTemp("", "key")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
config.Server.TLS.Certificate = certFile.Name()
|
||||
config.Server.TLS.Key = keyFile.Name()
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 0)
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenTLSClientCertificateDoesNotExist(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
certFile, err := os.CreateTemp("", "cert")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(certFile.Name())
|
||||
|
||||
keyFile, err := os.CreateTemp("", "key")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
config.Server.TLS.Certificate = certFile.Name()
|
||||
config.Server.TLS.Key = keyFile.Name()
|
||||
config.Server.TLS.ClientCertificates = []string{"/tmp/unexisting"}
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: client_certificates: certificates: file path /tmp/unexisting does not exist")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenTLSClientAuthIsDefinedButNotServerCertificate(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
certFile, err := os.CreateTemp("", "cert")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(certFile.Name())
|
||||
|
||||
config.Server.TLS.ClientCertificates = []string{certFile.Name()}
|
||||
|
||||
ValidateServer(&config, validator)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "server: tls: client authentication cannot be configured if no server certificate and key are provided")
|
||||
}
|
||||
|
||||
func TestShouldNotUpdateConfig(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
|
|
@ -40,6 +40,9 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
const schemeHTTP = "http"
|
||||
const schemeHTTPS = "https"
|
||||
|
||||
const (
|
||||
dev = "dev"
|
||||
f = "false"
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
|
||||
duoapi "github.com/duosecurity/duo_api_golang"
|
||||
"github.com/fasthttp/router"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
@ -15,7 +19,6 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/duo"
|
||||
"github.com/authelia/authelia/v4/internal/handlers"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
)
|
||||
|
||||
|
@ -178,10 +181,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
|||
return handler
|
||||
}
|
||||
|
||||
// Start Authelia's internal webserver with the given configuration and providers.
|
||||
func Start(configuration schema.Configuration, providers middlewares.Providers) {
|
||||
logger := logging.Logger()
|
||||
|
||||
// CreateServer Create Authelia's internal webserver with the given configuration and providers.
|
||||
func CreateServer(configuration schema.Configuration, providers middlewares.Providers) (*fasthttp.Server, net.Listener) {
|
||||
handler := registerRoutes(configuration, providers)
|
||||
|
||||
server := &fasthttp.Server{
|
||||
|
@ -191,36 +192,66 @@ func Start(configuration schema.Configuration, providers middlewares.Providers)
|
|||
ReadBufferSize: configuration.Server.ReadBufferSize,
|
||||
WriteBufferSize: configuration.Server.WriteBufferSize,
|
||||
}
|
||||
logger := logging.Logger()
|
||||
|
||||
address := net.JoinHostPort(configuration.Server.Host, strconv.Itoa(configuration.Server.Port))
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
logger.Fatalf("Error initializing listener: %s", err)
|
||||
}
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
connectionType string
|
||||
connectionScheme string
|
||||
)
|
||||
|
||||
if configuration.Server.TLS.Certificate != "" && configuration.Server.TLS.Key != "" {
|
||||
if err = writeHealthCheckEnv(configuration.Server.DisableHealthcheck, "https", configuration.Server.Host, configuration.Server.Path, configuration.Server.Port); err != nil {
|
||||
logger.Fatalf("Could not configure healthcheck: %v", err)
|
||||
connectionType, connectionScheme = "TLS", schemeHTTPS
|
||||
err = server.AppendCert(configuration.Server.TLS.Certificate, configuration.Server.TLS.Key)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatalf("unable to load certificate: %v", err)
|
||||
}
|
||||
|
||||
if configuration.Server.Path == "" {
|
||||
logger.Infof("Listening for TLS connections on '%s' path '/'", address)
|
||||
} else {
|
||||
logger.Infof("Listening for TLS connections on '%s' paths '/' and '%s'", address, configuration.Server.Path)
|
||||
if len(configuration.Server.TLS.ClientCertificates) > 0 {
|
||||
caCertPool := x509.NewCertPool()
|
||||
|
||||
for _, path := range configuration.Server.TLS.ClientCertificates {
|
||||
cert, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
logger.Fatalf("Cannot read client TLS certificate %s: %s", path, err)
|
||||
}
|
||||
|
||||
caCertPool.AppendCertsFromPEM(cert)
|
||||
}
|
||||
|
||||
// ClientCAs should never be nil, otherwise the system cert pool is used for client authentication
|
||||
// but we don't want everybody on the Internet to be able to authenticate.
|
||||
server.TLSConfig.ClientCAs = caCertPool
|
||||
server.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
logger.Fatal(server.ServeTLS(listener, configuration.Server.TLS.Certificate, configuration.Server.TLS.Key))
|
||||
listener, err = tls.Listen("tcp", address, server.TLSConfig.Clone())
|
||||
if err != nil {
|
||||
logger.Fatalf("Error initializing listener: %s", err)
|
||||
}
|
||||
} else {
|
||||
if err = writeHealthCheckEnv(configuration.Server.DisableHealthcheck, "http", configuration.Server.Host, configuration.Server.Path, configuration.Server.Port); err != nil {
|
||||
logger.Fatalf("Could not configure healthcheck: %v", err)
|
||||
connectionType, connectionScheme = "non-TLS", schemeHTTP
|
||||
listener, err = net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
logger.Fatalf("Error initializing listener: %s", err)
|
||||
}
|
||||
|
||||
if configuration.Server.Path == "" {
|
||||
logger.Infof("Listening for non-TLS connections on '%s' path '/'", address)
|
||||
} else {
|
||||
logger.Infof("Listening for non-TLS connections on '%s' paths '/' and '%s'", address, configuration.Server.Path)
|
||||
}
|
||||
logger.Fatal(server.Serve(listener))
|
||||
}
|
||||
|
||||
if err = writeHealthCheckEnv(configuration.Server.DisableHealthcheck, connectionScheme, configuration.Server.Host,
|
||||
configuration.Server.Path, configuration.Server.Port); err != nil {
|
||||
logger.Fatalf("Could not configure healthcheck: %v", err)
|
||||
}
|
||||
|
||||
actualAddress := listener.Addr().String()
|
||||
if configuration.Server.Path == "" {
|
||||
logger.Infof("Initializing server for %s connections on '%s' path '/'", connectionType, actualAddress)
|
||||
} else {
|
||||
logger.Infof("Initializing server for %s connections on '%s' paths '/' and '%s'", connectionType, actualAddress, configuration.Server.Path)
|
||||
}
|
||||
|
||||
return server, listener
|
||||
}
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// TemporaryCertificate contains the FD of 2 temporary files containing the PEM format of the certificate and private key.
|
||||
type TemporaryCertificate struct {
|
||||
CertFile *os.File
|
||||
KeyFile *os.File
|
||||
|
||||
Certificate *x509.Certificate
|
||||
|
||||
CertificatePEM []byte
|
||||
KeyPEM []byte
|
||||
}
|
||||
|
||||
func (tc TemporaryCertificate) TLSCertificate() (tls.Certificate, error) {
|
||||
return tls.LoadX509KeyPair(tc.CertFile.Name(), tc.KeyFile.Name())
|
||||
}
|
||||
|
||||
func (tc *TemporaryCertificate) Close() {
|
||||
if tc.CertFile != nil {
|
||||
tc.CertFile.Close()
|
||||
}
|
||||
|
||||
if tc.KeyFile != nil {
|
||||
tc.KeyFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type CertificateContext struct {
|
||||
Certificates []TemporaryCertificate
|
||||
privateKeyBuilder utils.PrivateKeyBuilder
|
||||
}
|
||||
|
||||
// NewCertificateContext instantiate a new certificate context used to easily generate certificates within tests.
|
||||
func NewCertificateContext(privateKeyBuilder utils.PrivateKeyBuilder) (*CertificateContext, error) {
|
||||
certificateContext := new(CertificateContext)
|
||||
certificateContext.privateKeyBuilder = privateKeyBuilder
|
||||
|
||||
cert, err := certificateContext.GenerateCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificateContext.Certificates = []TemporaryCertificate{*cert}
|
||||
|
||||
return certificateContext, nil
|
||||
}
|
||||
|
||||
// GenerateCertificate generate a new certificate in the context.
|
||||
func (cc *CertificateContext) GenerateCertificate() (*TemporaryCertificate, error) {
|
||||
certBytes, keyBytes, err := utils.GenerateCertificate(cc.privateKeyBuilder,
|
||||
[]string{"authelia.com", "example.org", "local.example.com"},
|
||||
time.Now(), 3*time.Hour, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate certificate: %v", err)
|
||||
}
|
||||
|
||||
tmpCertificate := new(TemporaryCertificate)
|
||||
|
||||
certFile, err := os.CreateTemp("", "cert")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create temp file for certificate: %v", err)
|
||||
}
|
||||
|
||||
tmpCertificate.CertFile = certFile
|
||||
tmpCertificate.CertificatePEM = certBytes
|
||||
|
||||
block, _ := pem.Decode(certBytes)
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse certificate: %v", err)
|
||||
}
|
||||
|
||||
tmpCertificate.Certificate = c
|
||||
|
||||
err = os.WriteFile(tmpCertificate.CertFile.Name(), certBytes, 0600)
|
||||
if err != nil {
|
||||
tmpCertificate.Close()
|
||||
return nil, fmt.Errorf("unable to write certificates in file: %v", err)
|
||||
}
|
||||
|
||||
keyFile, err := os.CreateTemp("", "key")
|
||||
if err != nil {
|
||||
tmpCertificate.Close()
|
||||
return nil, fmt.Errorf("unable to create temp file for private key: %v", err)
|
||||
}
|
||||
|
||||
tmpCertificate.KeyFile = keyFile
|
||||
tmpCertificate.KeyPEM = keyBytes
|
||||
|
||||
err = os.WriteFile(tmpCertificate.KeyFile.Name(), keyBytes, 0600)
|
||||
if err != nil {
|
||||
tmpCertificate.Close()
|
||||
return nil, fmt.Errorf("unable to write private key in file: %v", err)
|
||||
}
|
||||
|
||||
cc.Certificates = append(cc.Certificates, *tmpCertificate)
|
||||
|
||||
return tmpCertificate, nil
|
||||
}
|
||||
|
||||
func (cc *CertificateContext) Close() {
|
||||
for _, tc := range cc.Certificates {
|
||||
tc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type TLSServerContext struct {
|
||||
server *fasthttp.Server
|
||||
port int
|
||||
}
|
||||
|
||||
func NewTLSServerContext(configuration schema.Configuration) (*TLSServerContext, error) {
|
||||
serverContext := new(TLSServerContext)
|
||||
|
||||
s, listener := CreateServer(configuration, middlewares.Providers{})
|
||||
serverContext.server = s
|
||||
|
||||
go func() {
|
||||
err := s.Serve(listener)
|
||||
if err != nil {
|
||||
logging.Logger().Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
addrSplit := strings.Split(listener.Addr().String(), ":")
|
||||
if len(addrSplit) > 1 {
|
||||
port, err := strconv.ParseInt(addrSplit[len(addrSplit)-1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse port from address: %v", err)
|
||||
}
|
||||
|
||||
serverContext.port = int(port)
|
||||
}
|
||||
|
||||
return serverContext, nil
|
||||
}
|
||||
|
||||
func (sc *TLSServerContext) Port() int {
|
||||
return sc.port
|
||||
}
|
||||
|
||||
func (sc *TLSServerContext) Close() error {
|
||||
return sc.server.Shutdown()
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenClientDoesNotSkipVerify(t *testing.T) {
|
||||
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer certificateContext.Close()
|
||||
|
||||
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
||||
Server: schema.ServerConfiguration{
|
||||
TLS: schema.ServerTLSConfiguration{
|
||||
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
||||
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer tlsServerContext.Close()
|
||||
|
||||
fmt.Println(tlsServerContext.Port())
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d", tlsServerContext.Port()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = http.DefaultClient.Do(req)
|
||||
require.Error(t, err)
|
||||
|
||||
require.Contains(t, err.Error(), "x509: certificate signed by unknown authority")
|
||||
}
|
||||
|
||||
func TestShouldServeOverTLSWhenClientDoesSkipVerify(t *testing.T) {
|
||||
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer certificateContext.Close()
|
||||
|
||||
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
||||
Server: schema.ServerConfiguration{
|
||||
TLS: schema.ServerTLSConfiguration{
|
||||
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
||||
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer tlsServerContext.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // Needs to be enabled in tests. Not used in production.
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
_, err = io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "404 Not Found", res.Status)
|
||||
}
|
||||
|
||||
func TestShouldServeOverTLSWhenClientHasProperRootCA(t *testing.T) {
|
||||
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer certificateContext.Close()
|
||||
|
||||
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
||||
Server: schema.ServerConfiguration{
|
||||
TLS: schema.ServerTLSConfiguration{
|
||||
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
||||
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer tlsServerContext.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
block, _ := pem.Decode(certificateContext.Certificates[0].CertificatePEM)
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a root CA for the client to properly validate server cert.
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(c)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
_, err = io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "404 Not Found", res.Status)
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenMutualTLSIsConfiguredAndClientIsNotAuthenticated(t *testing.T) {
|
||||
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer certificateContext.Close()
|
||||
|
||||
clientCert, err := certificateContext.GenerateCertificate()
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
||||
Server: schema.ServerConfiguration{
|
||||
TLS: schema.ServerTLSConfiguration{
|
||||
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
||||
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
||||
ClientCertificates: []string{clientCert.CertFile.Name()},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer tlsServerContext.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a root CA for the client to properly validate server cert.
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(certificateContext.Certificates[0].Certificate)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
_, err = client.Do(req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "remote error: tls: bad certificate")
|
||||
}
|
||||
|
||||
func TestShouldServeProperlyWhenMutualTLSIsConfiguredAndClientIsAuthenticated(t *testing.T) {
|
||||
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
||||
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer certificateContext.Close()
|
||||
|
||||
clientCert, err := certificateContext.GenerateCertificate()
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
||||
Server: schema.ServerConfiguration{
|
||||
TLS: schema.ServerTLSConfiguration{
|
||||
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
||||
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
||||
ClientCertificates: []string{clientCert.CertFile.Name()},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer tlsServerContext.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a root CA for the client to properly validate server cert.
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(certificateContext.Certificates[0].Certificate)
|
||||
|
||||
cCert, err := certificateContext.Certificates[1].TLSCertificate()
|
||||
require.NoError(t, err)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
Certificates: []tls.Certificate{cCert},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
_, err = io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "404 Not Found", res.Status)
|
||||
}
|
|
@ -46,7 +46,7 @@ func waitUntilAutheliaBackendIsReady(dockerEnvironment *DockerEnvironment) error
|
|||
90*time.Second,
|
||||
dockerEnvironment,
|
||||
"authelia-backend",
|
||||
[]string{"Listening for"})
|
||||
[]string{"Initializing server for"})
|
||||
}
|
||||
|
||||
func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) error {
|
||||
|
|
|
@ -45,6 +45,10 @@ func (s *CLISuite) SetupTest() {
|
|||
}
|
||||
|
||||
func (s *CLISuite) TestShouldPrintBuildInformation() {
|
||||
if os.Getenv("CI") == "false" {
|
||||
s.T().Skip("Skipping testing in dev environment")
|
||||
}
|
||||
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Last Tag: ")
|
||||
|
@ -92,22 +96,22 @@ func (s *CLISuite) TestShouldHashPasswordSHA512() {
|
|||
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=127.0.0.1", "--dir=/tmp/"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--start-date='Jan 1 15:04:05 2011'"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
|
||||
|
@ -119,15 +123,15 @@ func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
|
|||
func (s *CLISuite) TestShouldGenerateCertificateCA() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ca"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ed25519"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
|
||||
|
@ -139,29 +143,29 @@ func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
|
|||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P224"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P256"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P384"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P521"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
s.Assert().Contains(output, "Certificate written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
|
||||
|
|
|
@ -1,17 +1,38 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
)
|
||||
|
||||
// PEMBlockType represent an enum of the existing PEM block types.
|
||||
type PEMBlockType int
|
||||
|
||||
const (
|
||||
// Certificate block type.
|
||||
Certificate PEMBlockType = iota
|
||||
// PrivateKey block type.
|
||||
PrivateKey
|
||||
)
|
||||
|
||||
// NewTLSConfig generates a tls.Config from a schema.TLSConfig and a x509.CertPool.
|
||||
func NewTLSConfig(config *schema.TLSConfig, defaultMinVersion uint16, certPool *x509.CertPool) (tlsConfig *tls.Config) {
|
||||
minVersion, err := TLSStringToTLSConfigVersion(config.MinimumVersion)
|
||||
|
@ -83,3 +104,150 @@ func TLSStringToTLSConfigVersion(input string) (version uint16, err error) {
|
|||
|
||||
return 0, ErrTLSVersionNotSupported
|
||||
}
|
||||
|
||||
// GenerateCertificate generate a certificate given a private key. RSA, Ed25519 and ECDSA are officially supported.
|
||||
func GenerateCertificate(privateKeyBuilder PrivateKeyBuilder, hosts []string, validFrom time.Time, validFor time.Duration, isCA bool) ([]byte, []byte, error) {
|
||||
privateKey, err := privateKeyBuilder.Build()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to build private key: %w", err)
|
||||
}
|
||||
|
||||
notBefore := validFrom
|
||||
notAfter := validFrom.Add(validFor)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, h)
|
||||
}
|
||||
}
|
||||
|
||||
if isCA {
|
||||
template.IsCA = true
|
||||
template.KeyUsage |= x509.KeyUsageCertSign
|
||||
}
|
||||
|
||||
certDERBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(privateKey), privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
certPEMBytes, err := ConvertDERToPEM(certDERBytes, Certificate)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("faile to convert certificate in DER format into PEM: %v", err)
|
||||
}
|
||||
|
||||
keyDERBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal private key: %v", err)
|
||||
}
|
||||
|
||||
keyPEMBytes, err := ConvertDERToPEM(keyDERBytes, PrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("faile to convert certificate in DER format into PEM: %v", err)
|
||||
}
|
||||
|
||||
return certPEMBytes, keyPEMBytes, nil
|
||||
}
|
||||
|
||||
// ConvertDERToPEM convert certificate in DER format into PEM format.
|
||||
func ConvertDERToPEM(der []byte, blockType PEMBlockType) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
var blockTypeStr string
|
||||
|
||||
switch blockType {
|
||||
case Certificate:
|
||||
blockTypeStr = "CERTIFICATE"
|
||||
case PrivateKey:
|
||||
blockTypeStr = "PRIVATE KEY"
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown PEM block type %d", blockType)
|
||||
}
|
||||
|
||||
if err := pem.Encode(&buf, &pem.Block{Type: blockTypeStr, Bytes: der}); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode DER data into PEM: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func publicKey(privateKey interface{}) interface{} {
|
||||
switch k := privateKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
case ed25519.PrivateKey:
|
||||
return k.Public().(ed25519.PublicKey)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateKeyBuilder interface for a private key builder.
|
||||
type PrivateKeyBuilder interface {
|
||||
Build() (interface{}, error)
|
||||
}
|
||||
|
||||
// RSAKeyBuilder builder of RSA private key.
|
||||
type RSAKeyBuilder struct {
|
||||
keySizeInBits int
|
||||
}
|
||||
|
||||
// WithKeySize configure the key size to use with RSA.
|
||||
func (rkb RSAKeyBuilder) WithKeySize(bits int) RSAKeyBuilder {
|
||||
rkb.keySizeInBits = bits
|
||||
return rkb
|
||||
}
|
||||
|
||||
// Build a RSA private key.
|
||||
func (rkb RSAKeyBuilder) Build() (interface{}, error) {
|
||||
return rsa.GenerateKey(rand.Reader, rkb.keySizeInBits)
|
||||
}
|
||||
|
||||
// Ed25519KeyBuilder builder of Ed25519 private key.
|
||||
type Ed25519KeyBuilder struct{}
|
||||
|
||||
// Build an Ed25519 private key.
|
||||
func (ekb Ed25519KeyBuilder) Build() (interface{}, error) {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
return priv, err
|
||||
}
|
||||
|
||||
// ECDSAKeyBuilder builder of ECDSA private key.
|
||||
type ECDSAKeyBuilder struct {
|
||||
curve elliptic.Curve
|
||||
}
|
||||
|
||||
// WithCurve configure the curve to use for the ECDSA private key.
|
||||
func (ekb ECDSAKeyBuilder) WithCurve(curve elliptic.Curve) ECDSAKeyBuilder {
|
||||
ekb.curve = curve
|
||||
return ekb
|
||||
}
|
||||
|
||||
// Build an ECDSA private key.
|
||||
func (ekb ECDSAKeyBuilder) Build() (interface{}, error) {
|
||||
return ecdsa.GenerateKey(ekb.curve, rand.Reader)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/tls"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -124,3 +126,44 @@ func TestShouldReadCertsFromDirectoryButNotKeys(t *testing.T) {
|
|||
|
||||
assert.EqualError(t, errors[0], "could not import certificate key.pem")
|
||||
}
|
||||
|
||||
func TestShouldGenerateCertificateAndPersistIt(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
PrivateKeyBuilder PrivateKeyBuilder
|
||||
}{
|
||||
{
|
||||
Name: "P224",
|
||||
PrivateKeyBuilder: ECDSAKeyBuilder{}.WithCurve(elliptic.P224()),
|
||||
},
|
||||
{
|
||||
Name: "P256",
|
||||
PrivateKeyBuilder: ECDSAKeyBuilder{}.WithCurve(elliptic.P256()),
|
||||
},
|
||||
{
|
||||
Name: "P384",
|
||||
PrivateKeyBuilder: ECDSAKeyBuilder{}.WithCurve(elliptic.P384()),
|
||||
},
|
||||
{
|
||||
Name: "P521",
|
||||
PrivateKeyBuilder: ECDSAKeyBuilder{}.WithCurve(elliptic.P521()),
|
||||
},
|
||||
{
|
||||
Name: "Ed25519",
|
||||
PrivateKeyBuilder: Ed25519KeyBuilder{},
|
||||
},
|
||||
{
|
||||
Name: "RSA",
|
||||
PrivateKeyBuilder: RSAKeyBuilder{keySizeInBits: 2048},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
certBytes, keyBytes, err := GenerateCertificate(tc.PrivateKeyBuilder, []string{"authelia.com", "example.org"}, time.Now(), 3*time.Hour, false)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, len(certBytes) > 0)
|
||||
assert.True(t, len(keyBytes) > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue