[FEATURE] Add command to generate self-signed certs in authelia binary. (#676)

* [FEATURE] Add command to generate self-signed certs in authelia binary.
* Apply suggestions from code review

Fixes #454 

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
pull/677/head
Clément Michaud 2020-03-01 14:08:09 +01:00 committed by GitHub
parent b911c2f0f3
commit 0c43740a4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 1 deletions

View File

@ -121,5 +121,8 @@ func main() {
} }
rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd, commands.MigrateCmd) rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd, commands.MigrateCmd)
rootCmd.Execute() rootCmd.AddCommand(commands.CertificatesCmd)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
} }

View File

@ -19,6 +19,18 @@ only two: a reverse proxy such as Nginx, Traefik or HAProxy and Authelia.
Documentation for deploying a reverse proxy collaborating with Authelia is available Documentation for deploying a reverse proxy collaborating with Authelia is available
[here](./supported-proxies/index.md). [here](./supported-proxies/index.md).
Please note that Authelia only works for websites served over HTTPS because the session cookie
can only be transmitted over secure connections. Therefore, if you need to generate a
self-signed certificate for your setup, you can use the dedicated helper function provided
by the authelia binary.
# Generate a certificate covering "example.com" for one year in the /tmp/certs/ directory.
$ docker run authelia/authelia authelia certificates generate --host example.com --dir /tmp/certs/
You can see all available options with the following command:
$ docker run authelia/authelia authelia certificates generate --help
## Discard components ## Discard components
### Discard SQL server ### Discard SQL server

View File

@ -46,3 +46,14 @@ Here is a description of the complete workflow:
<img src="../images/sequence-diagram.png"/> <img src="../images/sequence-diagram.png"/>
</p> </p>
## HTTP/HTTPS
Authelia only works for websites served over HTTPS because the session cookie can only be
transmitted over secure connections. Please note that it has been decided that we won't
support websites served over HTTP in order to avoid any risk due to misconfiguration.
(see [#590](https://github.com/authelia/authelia/issues/590)).
If a self-signed certificate is required, the following command can be used to generate one:
# Generate a certificate covering "example.com" for one year in the /tmp/certs/ directory.
$ docker run authelia/authelia authelia certificates generate --host example.com --dir /tmp/certs/

View File

@ -0,0 +1,182 @@
package commands
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"path"
"strings"
"time"
"github.com/spf13/cobra"
)
var (
host string
validFrom string
validFor time.Duration
isCA bool
rsaBits int
ecdsaCurve string
ed25519Key bool
targetDirectory string
)
func init() {
CertificatesGenerateCmd.PersistentFlags().StringVar(&host, "host", "", "Comma-separated hostnames and IPs to generate a certificate for")
err := CertificatesGenerateCmd.MarkPersistentFlagRequired("host")
if err != nil {
log.Fatal(err)
}
CertificatesGenerateCmd.PersistentFlags().StringVar(&validFrom, "start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
CertificatesGenerateCmd.PersistentFlags().DurationVar(&validFor, "duration", 365*24*time.Hour, "Duration that certificate is valid for")
CertificatesGenerateCmd.PersistentFlags().BoolVar(&isCA, "ca", false, "Whether this cert should be its own Certificate Authority")
CertificatesGenerateCmd.PersistentFlags().IntVar(&rsaBits, "rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
CertificatesGenerateCmd.PersistentFlags().StringVar(&ecdsaCurve, "ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
CertificatesGenerateCmd.PersistentFlags().BoolVar(&ed25519Key, "ed25519", false, "Generate an Ed25519 key")
CertificatesGenerateCmd.PersistentFlags().StringVar(&targetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
CertificatesCmd.AddCommand(CertificatesGenerateCmd)
}
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
}
}
func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
// implementation retrieved from https://golang.org/src/crypto/tls/generate_cert.go
var priv interface{}
var err error
switch ecdsaCurve {
case "":
if ed25519Key {
_, priv, err = ed25519.GenerateKey(rand.Reader)
} else {
priv, err = rsa.GenerateKey(rand.Reader, rsaBits)
}
case "P224":
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case "P384":
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
log.Fatalf("Unrecognized elliptic curve: %q", ecdsaCurve)
}
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
}
var notBefore time.Time
if len(validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("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},
BasicConstraintsValid: true,
}
hosts := strings.Split(host, ",")
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
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certPath := path.Join(targetDirectory, "cert.pem")
certOut, err := os.Create(certPath)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", certPath, err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("Failed to write data to cert.pem: %v", err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", certPath, err)
}
log.Printf("wrote %s\n", certPath)
keyPath := path.Join(targetDirectory, "key.pem")
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
return
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", keyPath, err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", keyPath, err)
}
log.Printf("wrote %s\n", keyPath)
}
var CertificatesCmd = &cobra.Command{
Use: "certificates",
Short: "Commands related to certificate generation",
}
var CertificatesGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a self-signed certificate",
Run: generateSelfSignedCertificate,
}