mirror of https://github.com/status-im/consul.git
Builtin tls helper (#5078)
* command: add tls subcommand * website: update docs and guide
This commit is contained in:
parent
564288adcc
commit
acc458d7a4
|
@ -70,6 +70,11 @@ import (
|
|||
snapinspect "github.com/hashicorp/consul/command/snapshot/inspect"
|
||||
snaprestore "github.com/hashicorp/consul/command/snapshot/restore"
|
||||
snapsave "github.com/hashicorp/consul/command/snapshot/save"
|
||||
"github.com/hashicorp/consul/command/tls"
|
||||
tlsca "github.com/hashicorp/consul/command/tls/ca"
|
||||
tlscacreate "github.com/hashicorp/consul/command/tls/ca/create"
|
||||
tlscert "github.com/hashicorp/consul/command/tls/cert"
|
||||
tlscertcreate "github.com/hashicorp/consul/command/tls/cert/create"
|
||||
"github.com/hashicorp/consul/command/validate"
|
||||
"github.com/hashicorp/consul/command/version"
|
||||
"github.com/hashicorp/consul/command/watch"
|
||||
|
@ -155,6 +160,11 @@ func init() {
|
|||
Register("snapshot inspect", func(ui cli.Ui) (cli.Command, error) { return snapinspect.New(ui), nil })
|
||||
Register("snapshot restore", func(ui cli.Ui) (cli.Command, error) { return snaprestore.New(ui), nil })
|
||||
Register("snapshot save", func(ui cli.Ui) (cli.Command, error) { return snapsave.New(ui), nil })
|
||||
Register("tls", func(ui cli.Ui) (cli.Command, error) { return tls.New(), nil })
|
||||
Register("tls ca", func(ui cli.Ui) (cli.Command, error) { return tlsca.New(), nil })
|
||||
Register("tls ca create", func(ui cli.Ui) (cli.Command, error) { return tlscacreate.New(ui), nil })
|
||||
Register("tls cert", func(ui cli.Ui) (cli.Command, error) { return tlscert.New(), nil })
|
||||
Register("tls cert create", func(ui cli.Ui) (cli.Command, error) { return tlscertcreate.New(ui), nil })
|
||||
Register("validate", func(ui cli.Ui) (cli.Command, error) { return validate.New(ui), nil })
|
||||
Register("version", func(ui cli.Ui) (cli.Command, error) { return version.New(ui, verHuman), nil })
|
||||
Register("watch", func(ui cli.Ui) (cli.Command, error) { return watch.New(ui, MakeShutdownCh()), nil })
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestCatalogCommand_noTabs(t *testing.T) {
|
||||
func TestConnectCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New().Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/hashicorp/consul/command/tls"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func New(ui cli.Ui) *cmd {
|
||||
c := &cmd{UI: ui}
|
||||
c.init()
|
||||
return c
|
||||
}
|
||||
|
||||
type cmd struct {
|
||||
UI cli.Ui
|
||||
flags *flag.FlagSet
|
||||
help string
|
||||
days int
|
||||
domain string
|
||||
constraint bool
|
||||
additionalConstraints flags.AppendSliceValue
|
||||
}
|
||||
|
||||
func (c *cmd) init() {
|
||||
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||
c.flags.IntVar(&c.days, "days", 1825, "Provide number of days the CA is valid for from now on. Defaults to 5 years.")
|
||||
c.flags.BoolVar(&c.constraint, "name-constraint", false, "Add name constraints for the CA. Results in rejecting "+
|
||||
"certificates for other DNS than specified. If turned on localhost and -domain will be added to the allowed "+
|
||||
"DNS. If the UI is going to be served over HTTPS its DNS has to be added with -additional-constraint. It is not "+
|
||||
"possible to add that after the fact! Defaults to false.")
|
||||
c.flags.StringVar(&c.domain, "domain", "consul", "Domain of consul cluster. Only used in combination with -name-constraint. Defaults to consul.")
|
||||
c.flags.Var(&c.additionalConstraints, "additional-name-constraint", "Add name constraints for the CA. Results in rejecting certificates "+
|
||||
"for other DNS than specified. Can be used multiple times. Only used in combination with -name-constraint.")
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
func (c *cmd) Run(args []string) int {
|
||||
if err := c.flags.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return 0
|
||||
}
|
||||
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
certFileName := fmt.Sprintf("%s-agent-ca.pem", c.domain)
|
||||
pkFileName := fmt.Sprintf("%s-agent-ca-key.pem", c.domain)
|
||||
|
||||
if !(tls.FileDoesNotExist(certFileName)) {
|
||||
c.UI.Error(certFileName + " already exists.")
|
||||
return 1
|
||||
}
|
||||
if !(tls.FileDoesNotExist(pkFileName)) {
|
||||
c.UI.Error(pkFileName + " already exists.")
|
||||
return 1
|
||||
}
|
||||
|
||||
sn, err := tls.GenerateSerialNumber()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
s, pk, err := tls.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
}
|
||||
constraints := []string{}
|
||||
if c.constraint {
|
||||
constraints = append(c.additionalConstraints, []string{c.domain, "localhost"}...)
|
||||
}
|
||||
ca, err := tls.GenerateCA(s, sn, c.days, constraints)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
}
|
||||
caFile, err := os.Create(certFileName)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
}
|
||||
caFile.WriteString(ca)
|
||||
c.UI.Output("==> Saved " + certFileName)
|
||||
pkFile, err := os.Create(pkFileName)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
}
|
||||
pkFile.WriteString(pk)
|
||||
c.UI.Output("==> Saved " + pkFileName)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
||||
func (c *cmd) Help() string {
|
||||
return c.help
|
||||
}
|
||||
|
||||
const synopsis = "Create a new consul CA"
|
||||
const help = `
|
||||
Usage: consul tls ca create [options]
|
||||
|
||||
Create a new consul CA:
|
||||
|
||||
$ consul tls ca create
|
||||
==> saved consul-agent-ca.pem
|
||||
==> saved consul-agent-ca-key.pem
|
||||
`
|
|
@ -0,0 +1,13 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New(nil).Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func New() *cmd {
|
||||
return &cmd{}
|
||||
}
|
||||
|
||||
type cmd struct{}
|
||||
|
||||
func (c *cmd) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
||||
func (c *cmd) Help() string {
|
||||
return flags.Usage(help, nil)
|
||||
}
|
||||
|
||||
const synopsis = `Helpers for CAs`
|
||||
const help = `
|
||||
Usage: consul tls ca <subcommand> [options] filename-prefix
|
||||
|
||||
This command has subcommands for interacting with Certificate Authorities.
|
||||
|
||||
Here are some simple examples, and more detailed examples are available
|
||||
in the subcommands or the documentation.
|
||||
|
||||
Create a CA
|
||||
|
||||
$ consul tls ca create
|
||||
==> saved consul-agent-ca.pem
|
||||
==> saved consul-agent-ca-key.pem
|
||||
|
||||
For more examples, ask for subcommand help or view the documentation.
|
||||
`
|
|
@ -0,0 +1,13 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New().Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/hashicorp/consul/command/tls"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func New(ui cli.Ui) *cmd {
|
||||
c := &cmd{UI: ui}
|
||||
c.init()
|
||||
return c
|
||||
}
|
||||
|
||||
type cmd struct {
|
||||
UI cli.Ui
|
||||
flags *flag.FlagSet
|
||||
ca string
|
||||
key string
|
||||
server bool
|
||||
client bool
|
||||
cli bool
|
||||
dc string
|
||||
days int
|
||||
domain string
|
||||
help string
|
||||
dnsnames flags.AppendSliceValue
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *cmd) init() {
|
||||
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||
c.flags.StringVar(&c.ca, "ca", "#DOMAINa#-agent-ca.pem", "Provide path to the ca. Defaults to #DOMAIN#-agent-ca.pem.")
|
||||
c.flags.StringVar(&c.key, "key", "#DOMAIN#-agent-ca-key.pem", "Provide path to the key. Defaults to #DOMAIN#-agent-ca-key.pem.")
|
||||
c.flags.BoolVar(&c.server, "server", false, "Generate server certificate.")
|
||||
c.flags.BoolVar(&c.client, "client", false, "Generate client certificate.")
|
||||
c.flags.BoolVar(&c.cli, "cli", false, "Generate cli certificate.")
|
||||
c.flags.IntVar(&c.days, "days", 365, "Provide number of days the certificate is valid for from now on. Defaults to 1 year.")
|
||||
c.flags.StringVar(&c.dc, "dc", "dc1", "Provide the datacenter. Matters only for -server certificates. Defaults to dc1.")
|
||||
c.flags.StringVar(&c.domain, "domain", "consul", "Provide the domain. Matters only for -server certificates.")
|
||||
c.flags.Var(&c.dnsnames, "additional-dnsname", "Provide an additional dnsname for Subject Alternative Names. "+
|
||||
"127.0.0.1 and localhost are always included. This flag may be provided multiple times.")
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
func (c *cmd) Run(args []string) int {
|
||||
if err := c.flags.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return 0
|
||||
}
|
||||
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
|
||||
return 1
|
||||
}
|
||||
if c.ca == "" {
|
||||
c.UI.Error("Please provide the ca")
|
||||
return 1
|
||||
}
|
||||
if c.key == "" {
|
||||
c.UI.Error("Please provide the key")
|
||||
return 1
|
||||
}
|
||||
|
||||
if !((c.server && !c.client && !c.cli) ||
|
||||
(!c.server && c.client && !c.cli) ||
|
||||
(!c.server && !c.client && c.cli)) {
|
||||
c.UI.Error("Please provide either -server, -client, or -cli")
|
||||
return 1
|
||||
}
|
||||
|
||||
var DNSNames []string
|
||||
var IPAddresses []net.IP
|
||||
var extKeyUsage []x509.ExtKeyUsage
|
||||
var name, prefix string
|
||||
|
||||
for _, d := range c.dnsnames {
|
||||
if len(d) > 0 {
|
||||
DNSNames = append(DNSNames, strings.TrimSpace(d))
|
||||
}
|
||||
}
|
||||
|
||||
if c.server {
|
||||
name = fmt.Sprintf("server.%s.%s", c.dc, c.domain)
|
||||
DNSNames = append(DNSNames, []string{name, "localhost"}...)
|
||||
IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
|
||||
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
||||
prefix = fmt.Sprintf("%s-server-%s", c.dc, c.domain)
|
||||
} else if c.client {
|
||||
name = fmt.Sprintf("client.%s.%s", c.dc, c.domain)
|
||||
DNSNames = append(DNSNames, []string{name, "localhost"}...)
|
||||
IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
|
||||
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
|
||||
prefix = fmt.Sprintf("%s-client-%s", c.dc, c.domain)
|
||||
} else if c.cli {
|
||||
name = fmt.Sprintf("cli.%s.%s", c.dc, c.domain)
|
||||
DNSNames = []string{name, "localhost"}
|
||||
prefix = fmt.Sprintf("%s-cli-%s", c.dc, c.domain)
|
||||
} else {
|
||||
c.UI.Error("Neither client, cli nor server - should not happen")
|
||||
return 1
|
||||
}
|
||||
|
||||
var pkFileName, certFileName string
|
||||
max := 10000
|
||||
for i := 0; i <= max; i++ {
|
||||
tmpCert := fmt.Sprintf("%s-%d.pem", prefix, i)
|
||||
tmpPk := fmt.Sprintf("%s-%d-key.pem", prefix, i)
|
||||
if tls.FileDoesNotExist(tmpCert) && tls.FileDoesNotExist(tmpPk) {
|
||||
certFileName = tmpCert
|
||||
pkFileName = tmpPk
|
||||
break
|
||||
}
|
||||
if i == max {
|
||||
c.UI.Error("Could not find a filename that doesn't already exist")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
caFile := strings.Replace(c.ca, "#DOMAIN#", c.domain, 1)
|
||||
keyFile := strings.Replace(c.key, "#DOMAIN#", c.domain, 1)
|
||||
cert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error reading CA: %s", err))
|
||||
return 1
|
||||
}
|
||||
key, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error reading CA key: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.server {
|
||||
c.UI.Info(
|
||||
`==> WARNING: Server Certificates grants authority to become a
|
||||
server and access all state in the cluster including root keys
|
||||
and all ACL tokens. Do not distribute them to production hosts
|
||||
that are not server nodes. Store them as securely as CA keys.`)
|
||||
}
|
||||
c.UI.Info("==> Using " + caFile + " and " + keyFile)
|
||||
|
||||
signer, err := tls.ParseSigner(string(key))
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
sn, err := tls.GenerateSerialNumber()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
pub, priv, err := tls.GenerateCert(signer, string(cert), sn, name, c.days, DNSNames, IPAddresses, extKeyUsage)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err = tls.Verify(string(cert), pub, name); err != nil {
|
||||
c.UI.Error("==> " + err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
certFile, err := os.Create(certFileName)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
certFile.WriteString(pub)
|
||||
c.UI.Output("==> Saved " + certFileName)
|
||||
|
||||
pkFile, err := os.Create(pkFileName)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
pkFile.WriteString(priv)
|
||||
c.UI.Output("==> Saved " + pkFileName)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
||||
func (c *cmd) Help() string {
|
||||
return c.help
|
||||
}
|
||||
|
||||
const synopsis = "Create a new certificate"
|
||||
const help = `
|
||||
Usage: consul tls cert create [options]
|
||||
|
||||
Create a new certificate
|
||||
|
||||
$ consul tls cert create -server
|
||||
==> WARNING: Server Certificates grants authority to become a
|
||||
server and access all state in the cluster including root keys
|
||||
and all ACL tokens. Do not distribute them to production hosts
|
||||
that are not server nodes. Store them as securely as CA keys.
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-server-dc1-0.pem
|
||||
==> Saved consul-server-dc1-0-key.pem
|
||||
$ consul tls cert -client
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-client-dc1-0.pem
|
||||
==> Saved consul-client-dc1-0-key.pem
|
||||
`
|
|
@ -0,0 +1,13 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New(nil).Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package cert
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func New() *cmd {
|
||||
return &cmd{}
|
||||
}
|
||||
|
||||
type cmd struct{}
|
||||
|
||||
func (c *cmd) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
||||
func (c *cmd) Help() string {
|
||||
return flags.Usage(help, nil)
|
||||
}
|
||||
|
||||
const synopsis = `Helpers for certificates`
|
||||
const help = `
|
||||
Usage: consul tls cert <subcommand> [options] [filename-prefix]
|
||||
|
||||
This command has subcommands for interacting with certificates
|
||||
|
||||
Here are some simple examples, and more detailed examples are available
|
||||
in the subcommands or the documentation.
|
||||
|
||||
Create a certificate
|
||||
|
||||
$ consul tls cert create -server
|
||||
==> saved consul-server-dc1.pem
|
||||
==> saved consul-server-dc1-key.pem
|
||||
|
||||
Create a certificate with your own CA:
|
||||
|
||||
$ consul tls cert create -server -ca-file my-ca.pem -ca-key-file my-ca-key.pem
|
||||
==> saved consul-server-dc1.pem
|
||||
==> saved consul-server-dc1-key.pem
|
||||
|
||||
For more examples, ask for subcommand help or view the documentation.
|
||||
`
|
|
@ -0,0 +1,13 @@
|
|||
package cert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New().Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateSerialNumber returns random bigint generated with crypto/rand
|
||||
func GenerateSerialNumber() (*big.Int, error) {
|
||||
l := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
s, err := rand.Int(rand.Reader, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GeneratePrivateKey generates a new ecdsa private key
|
||||
func GeneratePrivateKey() (crypto.Signer, string, error) {
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error generating private key: %s", err)
|
||||
}
|
||||
|
||||
bs, err := x509.MarshalECPrivateKey(pk)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error generating private key: %s", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs})
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error encoding private key: %s", err)
|
||||
}
|
||||
|
||||
return pk, buf.String(), nil
|
||||
}
|
||||
|
||||
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
|
||||
func GenerateCA(signer crypto.Signer, sn *big.Int, days int, constraints []string) (string, error) {
|
||||
id, err := keyID(signer.Public())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("Consul Agent CA %d", sn)
|
||||
|
||||
// Create the CA cert
|
||||
template := x509.Certificate{
|
||||
SerialNumber: sn,
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
PostalCode: []string{"94105"},
|
||||
Province: []string{"CA"},
|
||||
Locality: []string{"San Francisco"},
|
||||
StreetAddress: []string{"101 Second Street"},
|
||||
Organization: []string{"HashiCorp Inc."},
|
||||
CommonName: name,
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
|
||||
IsCA: true,
|
||||
NotAfter: time.Now().AddDate(0, 0, days),
|
||||
NotBefore: time.Now(),
|
||||
AuthorityKeyId: id,
|
||||
SubjectKeyId: id,
|
||||
}
|
||||
|
||||
if len(constraints) > 0 {
|
||||
template.PermittedDNSDomainsCritical = true
|
||||
template.PermittedDNSDomains = constraints
|
||||
}
|
||||
bs, err := x509.CreateCertificate(
|
||||
rand.Reader, &template, &template, signer.Public(), signer)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error generating CA certificate: %s", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error encoding private key: %s", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)
|
||||
func GenerateCert(signer crypto.Signer, ca string, sn *big.Int, name string, days int, DNSNames []string, IPAddresses []net.IP, extKeyUsage []x509.ExtKeyUsage) (string, string, error) {
|
||||
parent, err := parseCert(ca)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
signee, pk, err := GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
id, err := keyID(signee.Public())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: sn,
|
||||
Subject: pkix.Name{CommonName: name},
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: extKeyUsage,
|
||||
IsCA: false,
|
||||
NotAfter: time.Now().AddDate(0, 0, days),
|
||||
NotBefore: time.Now(),
|
||||
SubjectKeyId: id,
|
||||
DNSNames: DNSNames,
|
||||
IPAddresses: IPAddresses,
|
||||
}
|
||||
|
||||
bs, err := x509.CreateCertificate(rand.Reader, &template, parent, signee.Public(), signer)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error encoding private key: %s", err)
|
||||
}
|
||||
|
||||
return buf.String(), pk, nil
|
||||
}
|
||||
|
||||
// KeyId returns a x509 KeyId from the given signing key.
|
||||
func keyID(raw interface{}) ([]byte, error) {
|
||||
switch raw.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid key type: %T", raw)
|
||||
}
|
||||
|
||||
// This is not standard; RFC allows any unique identifier as long as they
|
||||
// match in subject/authority chains but suggests specific hashing of DER
|
||||
// bytes of public key including DER tags.
|
||||
bs, err := x509.MarshalPKIXPublicKey(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// String formatted
|
||||
kID := sha256.Sum256(bs)
|
||||
return []byte(strings.Replace(fmt.Sprintf("% x", kID), " ", ":", -1)), nil
|
||||
}
|
||||
|
||||
func parseCert(pemValue string) (*x509.Certificate, error) {
|
||||
// The _ result below is not an error but the remaining PEM bytes.
|
||||
block, _ := pem.Decode([]byte(pemValue))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no PEM-encoded data found")
|
||||
}
|
||||
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
|
||||
}
|
||||
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key
|
||||
// is expected to be the first block in the PEM value.
|
||||
func ParseSigner(pemValue string) (crypto.Signer, error) {
|
||||
// The _ result below is not an error but the remaining PEM bytes.
|
||||
block, _ := pem.Decode([]byte(pemValue))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no PEM-encoded data found")
|
||||
}
|
||||
|
||||
switch block.Type {
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(block.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func Verify(caString, certString, dns string) error {
|
||||
roots := x509.NewCertPool()
|
||||
ok := roots.AppendCertsFromPEM([]byte(caString))
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to parse root certificate")
|
||||
}
|
||||
|
||||
cert, err := parseCert(certString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate")
|
||||
}
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: fmt.Sprintf(dns),
|
||||
Roots: roots,
|
||||
}
|
||||
|
||||
_, err = cert.Verify(opts)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSerialNumber(t *testing.T) {
|
||||
n1, err := GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
|
||||
n2, err := GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
require.NotEqual(t, n1, n2)
|
||||
|
||||
n3, err := GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
require.NotEqual(t, n1, n3)
|
||||
require.NotEqual(t, n2, n3)
|
||||
|
||||
}
|
||||
|
||||
func TestGeneratePrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, p, err := GeneratePrivateKey()
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, p)
|
||||
require.Contains(t, p, "BEGIN EC PRIVATE KEY")
|
||||
require.Contains(t, p, "END EC PRIVATE KEY")
|
||||
|
||||
block, _ := pem.Decode([]byte(p))
|
||||
pk, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, pk)
|
||||
require.Equal(t, 256, pk.Params().BitSize)
|
||||
}
|
||||
|
||||
type TestSigner struct {
|
||||
public interface{}
|
||||
}
|
||||
|
||||
func (s *TestSigner) Public() crypto.PublicKey {
|
||||
return s.public
|
||||
}
|
||||
|
||||
func (s *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func TestGenerateCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
sn, err := GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
var s crypto.Signer
|
||||
|
||||
// test what happens without key
|
||||
s = &TestSigner{}
|
||||
ca, err := GenerateCA(s, sn, 0, nil)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, ca)
|
||||
|
||||
// test what happens with wrong key
|
||||
s = &TestSigner{public: &rsa.PublicKey{}}
|
||||
ca, err = GenerateCA(s, sn, 0, nil)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, ca)
|
||||
|
||||
// test what happens with correct key
|
||||
s, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.Nil(t, err)
|
||||
ca, err = GenerateCA(s, sn, 365, nil)
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, ca)
|
||||
|
||||
cert, err := parseCert(ca)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, fmt.Sprintf("Consul Agent CA %d", sn), cert.Subject.CommonName)
|
||||
require.Equal(t, true, cert.IsCA)
|
||||
require.Equal(t, true, cert.BasicConstraintsValid)
|
||||
|
||||
// format so that we don't take anything smaller than second into account.
|
||||
require.Equal(t, cert.NotBefore.Format(time.ANSIC), time.Now().UTC().Format(time.ANSIC))
|
||||
require.Equal(t, cert.NotAfter.Format(time.ANSIC), time.Now().AddDate(1, 0, 0).UTC().Format(time.ANSIC))
|
||||
|
||||
require.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageDigitalSignature, cert.KeyUsage)
|
||||
}
|
||||
|
||||
func TestGenerateCert(t *testing.T) {
|
||||
t.Parallel()
|
||||
sn, err := GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.Nil(t, err)
|
||||
ca, err := GenerateCA(signer, sn, 365, nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
sn, err = GenerateSerialNumber()
|
||||
require.Nil(t, err)
|
||||
DNSNames := []string{"server.dc1.consul"}
|
||||
IPAddresses := []net.IP{net.ParseIP("123.234.243.213")}
|
||||
extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
|
||||
name := "Cert Name"
|
||||
certificate, pk, err := GenerateCert(signer, ca, sn, name, 365, DNSNames, IPAddresses, extKeyUsage)
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, certificate)
|
||||
require.NotEmpty(t, pk)
|
||||
|
||||
cert, err := parseCert(certificate)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, name, cert.Subject.CommonName)
|
||||
require.Equal(t, true, cert.BasicConstraintsValid)
|
||||
signee, err := ParseSigner(pk)
|
||||
require.Nil(t, err)
|
||||
certID, err := keyID(signee.Public())
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, certID, cert.SubjectKeyId)
|
||||
caID, err := keyID(signer.Public())
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, caID, cert.AuthorityKeyId)
|
||||
require.Contains(t, cert.Issuer.CommonName, "Consul Agent CA")
|
||||
require.Equal(t, false, cert.IsCA)
|
||||
|
||||
// format so that we don't take anything smaller than second into account.
|
||||
require.Equal(t, cert.NotBefore.Format(time.ANSIC), time.Now().UTC().Format(time.ANSIC))
|
||||
require.Equal(t, cert.NotAfter.Format(time.ANSIC), time.Now().AddDate(1, 0, 0).UTC().Format(time.ANSIC))
|
||||
|
||||
require.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, cert.KeyUsage)
|
||||
require.Equal(t, extKeyUsage, cert.ExtKeyUsage)
|
||||
|
||||
// https://github.com/golang/go/blob/10538a8f9e2e718a47633ac5a6e90415a2c3f5f1/src/crypto/x509/verify.go#L414
|
||||
require.Equal(t, DNSNames, cert.DNSNames)
|
||||
require.True(t, IPAddresses[0].Equal(cert.IPAddresses[0]))
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func New() *cmd {
|
||||
return &cmd{}
|
||||
}
|
||||
|
||||
type cmd struct{}
|
||||
|
||||
func (c *cmd) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
||||
func (c *cmd) Help() string {
|
||||
return flags.Usage(help, nil)
|
||||
}
|
||||
|
||||
func FileDoesNotExist(file string) bool {
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const synopsis = `Builtin helpers for creating CAs and certificates`
|
||||
const help = `
|
||||
Usage: consul tls <subcommand> <subcommand> [options]
|
||||
|
||||
This command has subcommands for interacting with Consul TLS.
|
||||
|
||||
Here are some simple examples, and more detailed examples are available
|
||||
in the subcommands or the documentation.
|
||||
|
||||
Create a CA
|
||||
|
||||
$ consul tls ca create
|
||||
|
||||
Create a server certificate
|
||||
|
||||
$ consul tls cert create -server
|
||||
|
||||
Create a client certificate
|
||||
|
||||
$ consul tls cert create -client
|
||||
|
||||
For more examples, ask for subcommand help or view the documentation.
|
||||
`
|
|
@ -0,0 +1,13 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCommand_noTabs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if strings.ContainsRune(New().Help(), '\t') {
|
||||
t.Fatal("help has tabs")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Commands: TLS"
|
||||
sidebar_current: "docs-commands-tls"
|
||||
---
|
||||
|
||||
# Consul TLS
|
||||
|
||||
Command: `consul tls`
|
||||
|
||||
The `tls` command is used to help with setting up a CA and certificates for Consul TLS.
|
||||
|
||||
## Basic Examples
|
||||
|
||||
Create a CA:
|
||||
|
||||
```text
|
||||
$ consul tls ca create
|
||||
==> Saved consul-agent-ca.pem
|
||||
==> Saved consul-agent-ca-key.pem
|
||||
```
|
||||
|
||||
Create a client certificate:
|
||||
|
||||
```text
|
||||
$ consul tls cert create -client
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-client-dc1-0.pem
|
||||
==> Saved consul-client-dc1-0-key.pem
|
||||
```
|
||||
|
||||
For more examples, ask for subcommand help or view the subcommand documentation
|
||||
by clicking on one of the links in the sidebar.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: `consul tls <subcommand> <subcommand> [options]`
|
||||
|
||||
For the exact documentation for your Consul version, run `consul tls -h` to
|
||||
view the complete list of subcommands.
|
||||
|
||||
```text
|
||||
Usage: consul tls <subcommand> <subcommand> [options]
|
||||
|
||||
# ...
|
||||
|
||||
Subcommands:
|
||||
ca Helpers for CAs
|
||||
cert Helpers for certificates
|
||||
```
|
||||
|
||||
For more information, examples, and usage about a subcommand, click on the name
|
||||
of the subcommand in the sidebar or one of the links below:
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Commands: TLS CA Create"
|
||||
sidebar_current: "docs-commands-tls-ca"
|
||||
---
|
||||
|
||||
# Consul TLS CA Create
|
||||
|
||||
Command: `consul tls ca create`
|
||||
|
||||
This command create a self signed CA to be used for Consul TLS setup.
|
||||
|
||||
## Example
|
||||
|
||||
Create CA:
|
||||
|
||||
```bash
|
||||
$ consul tls ca create
|
||||
==> Saved consul-ca.pem
|
||||
==> Saved consul-ca-key.pem
|
||||
```
|
||||
|
||||
## Usage
|
||||
Usage: `consul tls ca create [filename-prefix] [options]`
|
||||
|
||||
#### TLS CA Create Options
|
||||
|
||||
- `-days=<int>` - Provide number of days the CA is valid for from now on, defaults to 5 years.
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Commands: TLS Cert Create"
|
||||
sidebar_current: "docs-commands-tls-cert"
|
||||
---
|
||||
|
||||
# Consul TLS Cert Create
|
||||
|
||||
Command: `consul tls cert create`
|
||||
|
||||
The `tls cert create` command is used to create certificates for your Consul TLS
|
||||
setup.
|
||||
|
||||
## Examples
|
||||
|
||||
Create a certificate for servers:
|
||||
|
||||
```bash
|
||||
$ consul tls cert create -server
|
||||
==> WARNING: Server Certificates grants authority to become a
|
||||
server and access all state in the cluster including root keys
|
||||
and all ACL tokens. Do not distribute them to production hosts
|
||||
that are not server nodes. Store them as securely as CA keys.
|
||||
==> Using consul-ca.pem and consul-ca-key.pem
|
||||
==> Saved consul-server-dc1-0.pem
|
||||
==> Saved consul-server-dc1-0-key.pem
|
||||
```
|
||||
|
||||
Create a certificate for clients:
|
||||
|
||||
```bash
|
||||
$ consul tls cert create -client
|
||||
==> Using consul-ca.pem and consul-ca-key.pem
|
||||
==> Saved consul-client-0.pem
|
||||
==> Saved consul-client-0-key.pem
|
||||
```
|
||||
|
||||
Create a certificate for cli:
|
||||
|
||||
```bash
|
||||
$ consul tls cert create -cli
|
||||
==> Using consul-ca.pem and consul-ca-key.pem
|
||||
==> Saved consul-cli-0.pem
|
||||
==> Saved consul-cli-0-key.pem
|
||||
```
|
||||
## Usage
|
||||
|
||||
Usage: `consul tls cert create [filename-prefix] [options]`
|
||||
|
||||
#### TLS Cert Create Options
|
||||
|
||||
- `-additional-dnsname=<string>` - Provide additional dnsname for Subject Alternative Names.
|
||||
|
||||
- `-ca=<string>` - Provide path to the ca
|
||||
|
||||
- `-cli` - Generate cli certificate
|
||||
|
||||
- `-client` - Generate client certificate
|
||||
|
||||
- `-days=<int>` - Provide number of days the certificate is valid for from now on.
|
||||
|
||||
- `-dc=<string>` - Provide the datacenter. Matters only for -server certificates
|
||||
|
||||
- `-domain=<string>` - Provide the domain. Matters only for -server certificates
|
||||
|
||||
- `-key=<string>` - Provide path to the key
|
||||
|
||||
- `-server` - Generate server certificate
|
|
@ -1,20 +1,53 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Creating Certificates"
|
||||
page_title: "Creating and Configuring TLS Certificates"
|
||||
sidebar_current: "docs-guides-creating-certificates"
|
||||
description: |-
|
||||
Learn how to create certificates for Consul.
|
||||
---
|
||||
|
||||
# Creating Certificates
|
||||
# Creating and Configuring TLS Certificates
|
||||
|
||||
Correctly configuring TLS can be a complex process, especially given the wide
|
||||
range of deployment methodologies. This guide will provide you with a
|
||||
production ready TLS configuration.
|
||||
Setting you cluster up with TLS is an important step towards a secure
|
||||
deployment. Correct TLS configuration is a prerequisite of our [Security
|
||||
Model](/docs/internals/security.html). Correctly configuring TLS can be a
|
||||
complex process however, especially given the wide range of deployment
|
||||
methodologies. This guide will provide you with a production ready TLS
|
||||
configuration.
|
||||
|
||||
~> Note that while Consul's TLS configuration will be production ready, key
|
||||
management and rotation is a complex subject not covered by this guide.
|
||||
[Vault][vault] is the suggested solution for key generation and management.
|
||||
~> More advanced topics like key management and rotation are not covered by this
|
||||
guide. [Vault][vault] is the suggested solution for key generation and
|
||||
management.
|
||||
|
||||
This guide has the following chapters:
|
||||
|
||||
1. [Creating Certificates](#creating-certificates)
|
||||
1. [Configuring Agents](#configuring-agents)
|
||||
1. [Configuring the Consul CLI for HTTPS](#configuring-the-consul-cli-for-https)
|
||||
1. [Configuring the Consul UI for HTTPS](#configuring-the-consul-ui-for-https)
|
||||
|
||||
This guide is structured in way that you build knowledge with every step. It is
|
||||
recommended to read the whole guide before starting with the actual work,
|
||||
because you can save time if you are aware of some of the more advanced things
|
||||
in Chapter [3](#configuring-the-consul-cli-for-https) and
|
||||
[4](#configuring-the-consul-ui-for-https).
|
||||
|
||||
### Reference Material
|
||||
|
||||
- [Encryption](/docs/agent/encryption.html)
|
||||
- [Security Model](/docs/internals/security.html)
|
||||
|
||||
## Creating Certificates
|
||||
|
||||
### Estimated Time to Complete
|
||||
|
||||
2 minutes
|
||||
|
||||
### Prerequisites
|
||||
|
||||
This guide assumes you have Consul 1.4.1 (or newer) in your PATH.
|
||||
|
||||
### Introduction
|
||||
|
||||
The first step to configuring TLS for Consul is generating certificates. In
|
||||
order to prevent unauthorized cluster access, Consul requires all certificates
|
||||
|
@ -22,162 +55,213 @@ be signed by the same Certificate Authority (CA). This should be a _private_ CA
|
|||
and not a public one like [Let's Encrypt][letsencrypt] as any certificate
|
||||
signed by this CA will be allowed to communicate with the cluster.
|
||||
|
||||
~> Consul certificates may be signed by intermediate CAs as long as the root CA
|
||||
is the same. Append all intermediate CAs to the `cert_file`.
|
||||
|
||||
|
||||
## Reference Material
|
||||
|
||||
- [Encryption](/docs/agent/encryption.html)
|
||||
|
||||
## Estimated Time to Complete
|
||||
|
||||
20 minutes
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This guide assumes you have [cfssl][cfssl] installed (be sure to install
|
||||
cfssljson as well).
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Create Certificate Authority
|
||||
### Step 1: Create a Certificate Authority
|
||||
|
||||
There are a variety of tools for managing your own CA, [like the PKI secret
|
||||
backend in Vault][vault-pki], but for the sake of simplicity this guide will
|
||||
use [cfssl][cfssl]. You can generate a private CA certificate and key with
|
||||
[cfssl][cfssl]:
|
||||
use Consul's builtin TLS helpers:
|
||||
|
||||
```shell
|
||||
# Generate a default CSR
|
||||
$ cfssl print-defaults csr > ca-csr.json
|
||||
$ consul tls ca create
|
||||
==> Saved consul-agent-ca.pem
|
||||
==> Saved consul-agent-ca-key.pem
|
||||
```
|
||||
Change the `key` field to use RSA with a size of 2048
|
||||
|
||||
The CA certificate (`consul-agent-ca.pem`) contains the public key necessary to
|
||||
validate Consul certificates and therefore must be distributed to every node
|
||||
that runs a consul agent.
|
||||
|
||||
~> The CA key (`consul-agent-ca-key.pem`) will be used to sign certificates for Consul
|
||||
nodes and must be kept private. Possession of this key allows anyone to run Consul as
|
||||
a trusted server and access all Consul data including ACL tokens.
|
||||
|
||||
|
||||
### Step 2: Create individual Server Certificates
|
||||
|
||||
Create a server certificate for datacenter `dc1` and domain `consul`, if your
|
||||
datacenter or domain is different please use the appropriate flags:
|
||||
|
||||
```shell
|
||||
$ consul tls cert create -server
|
||||
==> WARNING: Server Certificates grants authority to become a
|
||||
server and access all state in the cluster including root keys
|
||||
and all ACL tokens. Do not distribute them to production hosts
|
||||
that are not server nodes. Store them as securely as CA keys.
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-server-dc1-0.pem
|
||||
==> Saved consul-server-dc1-0-key.pem
|
||||
```
|
||||
|
||||
Please repeat this process until there is an *individual* certificate for each
|
||||
server. The command can be called over and over again, it will automatically add
|
||||
a suffix.
|
||||
|
||||
In order to authenticate Consul servers, servers are provided with a special
|
||||
certificate - one that contains `server.dc1.consul` in the `Subject Alternative
|
||||
Name`. If you enable
|
||||
[`verify_server_hostname`](/docs/agent/options.html#verify_server_hostname),
|
||||
only agents that provide such certificate are allowed to boot as a server.
|
||||
Without `verify_server_hostname = true` an attacker could compromise a Consul
|
||||
client agent and restart the agent as a server in order to get access to all the
|
||||
data in your cluster! This is why server certificates are special, and only
|
||||
servers should have them provisioned.
|
||||
|
||||
~> Server keys, like the CA key, must be kept private - they effectively allow
|
||||
access to all Consul data.
|
||||
|
||||
### Step 3: Create Client Certificates
|
||||
|
||||
Create a client certificate:
|
||||
|
||||
```shell
|
||||
$ consul tls cert create -client
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-client-dc1-0.pem
|
||||
==> Saved consul-client-dc1-0-key.pem
|
||||
```
|
||||
|
||||
Client certificates are also signed by your CA, but they do not have that
|
||||
special `Subject Alternative Name` which means that if `verify_server_hostname`
|
||||
is enabled, they cannot start as a server.
|
||||
|
||||
## Configuring Agents
|
||||
|
||||
### Prerequisites
|
||||
|
||||
For this section you need access to your existing or new Consul cluster and have
|
||||
the certificates from the previous chapters available.
|
||||
|
||||
### Notes on example configurations
|
||||
|
||||
The example configurations from this as well as the following chapters are in
|
||||
json. You can copy each one of the examples in its own file in a directory
|
||||
([`-config-dir`](/docs/agent/options.html#_config_dir)) from where consul will
|
||||
load all the configuration. This is just one way to do it, you can also put them
|
||||
all into one file if you prefer that.
|
||||
|
||||
### Introduction
|
||||
|
||||
By now you have created the certificates you need to enable TLS in your cluster.
|
||||
The next steps show how to configure TLS for a brand new cluster. If you already
|
||||
have a cluster in production without TLS please see the [encryption
|
||||
guide][guide] for the steps needed to introduce TLS without downtime.
|
||||
|
||||
### Step 1: Setup Consul servers with certificates
|
||||
|
||||
This step describes how to setup one of your consul servers, you want to make
|
||||
sure to repeat the process for the other ones as well with their individual
|
||||
certificates.
|
||||
|
||||
The following files need to be copied to your Consul server:
|
||||
|
||||
* `consul-agent-ca.pem`: CA public certificate.
|
||||
* `consul-server-dc1-0.pem`: Consul server node public certificate for the `dc1` datacenter.
|
||||
* `consul-server-dc1-0-key.pem`: Consul server node private key for the `dc1` datacenter.
|
||||
|
||||
Here is an example agent TLS configuration for Consul servers which mentions the
|
||||
copied files:
|
||||
|
||||
```json
|
||||
{
|
||||
"CN": "example.net",
|
||||
"hosts": [
|
||||
"example.net",
|
||||
"www.example.net"
|
||||
],
|
||||
"key": {
|
||||
"algo": "rsa",
|
||||
"size": 2048
|
||||
},
|
||||
"names": [
|
||||
{
|
||||
"C": "US",
|
||||
"ST": "CA",
|
||||
"L": "San Francisco"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
# Generate the CA's private key and certificate
|
||||
$ cfssl gencert -initca ca-csr.json | cfssljson -bare consul-ca
|
||||
```
|
||||
|
||||
The CA key (`consul-ca-key.pem`) will be used to sign certificates for Consul
|
||||
nodes and must be kept private. The CA certificate (`consul-ca.pem`) contains
|
||||
the public key necessary to validate Consul certificates and therefore must be
|
||||
distributed to every node that requires access.
|
||||
|
||||
### Step 2: Generate and Sign Node Certificates
|
||||
|
||||
Once you have a CA certificate and key you can generate and sign the
|
||||
certificates Consul will use directly. TLS certificates commonly use the
|
||||
fully-qualified domain name of the system being identified as the certificate's
|
||||
Common Name (CN). However, hosts (and therefore hostnames and IPs) are often
|
||||
ephemeral in Consul clusters. Not only would signing a new certificate per
|
||||
Consul node be difficult, but using a hostname provides no security or
|
||||
functional benefits to Consul. To fulfill the desired security properties
|
||||
(above) Consul certificates are signed with their region and role such as:
|
||||
|
||||
* `client.global.consul` for a client node in the `global` region
|
||||
* `server.us-west.consul` for a server node in the `us-west` region
|
||||
|
||||
To create certificates for the client and server in the cluster with
|
||||
[cfssl][cfssl], create the following configuration file as `cfssl.json` to increase the default certificate expiration time:
|
||||
|
||||
```json
|
||||
{
|
||||
"signing": {
|
||||
"default": {
|
||||
"expiry": "87600h",
|
||||
"usages": [
|
||||
"signing",
|
||||
"key encipherment",
|
||||
"server auth",
|
||||
"client auth"
|
||||
]
|
||||
}
|
||||
"verify_incoming": true,
|
||||
"verify_outgoing": true,
|
||||
"verify_server_hostname": true,
|
||||
"ca_file": "consul-agent-ca.pem",
|
||||
"cert_file": "consul-server-dc1-0.pem",
|
||||
"key_file": "consul-server-dc1-0-key.pem",
|
||||
"ports": {
|
||||
"http": -1,
|
||||
"https": 8501
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
# Generate a certificate for the Consul server
|
||||
$ echo '{"key":{"algo":"rsa","size":2048}}' | cfssl gencert -ca=consul-ca.pem -ca-key=consul-ca-key.pem -config=cfssl.json \
|
||||
-hostname="server.global.consul,localhost,127.0.0.1" - | cfssljson -bare server
|
||||
This configuration disables the HTTP port to make sure there is only encryted
|
||||
communication. Existing clients that are not yet prepared to talk HTTPS won't be
|
||||
able to connect afterwards. This also affects builtin tooling like `consul
|
||||
members` and the UI. The next chapters will demonstrate how to setup secure
|
||||
access.
|
||||
|
||||
# Generate a certificate for the Consul client
|
||||
$ echo '{"key":{"algo":"rsa","size":2048}}' | cfssl gencert -ca=consul-ca.pem -ca-key=consul-ca-key.pem -config=cfssl.json \
|
||||
-hostname="client.global.consul,localhost,127.0.0.1" - | cfssljson -bare client
|
||||
After a Consul agent restart, your servers should be only talking TLS.
|
||||
|
||||
# Generate a certificate for the CLI
|
||||
$ echo '{"key":{"algo":"rsa","size":2048}}' | cfssl gencert -ca=consul-ca.pem -ca-key=consul-ca-key.pem -profile=client \
|
||||
- | cfssljson -bare cli
|
||||
### Step 2: Setup Consul clients with certificates
|
||||
|
||||
Now copy the following files to your Consul clients:
|
||||
|
||||
* `consul-agent-ca.pem`: CA public certificate.
|
||||
* `consul-client-dc1-0.pem`: Consul client node public certificate.
|
||||
* `consul-client-dc1-0-key.pem`: Consul client node private key.
|
||||
|
||||
Here is an example agent TLS configuration for Consul agents which mentions the
|
||||
copied files:
|
||||
|
||||
```json
|
||||
{
|
||||
"verify_incoming": true,
|
||||
"verify_outgoing": true,
|
||||
"verify_server_hostname": true,
|
||||
"ca_file": "consul-agent-ca.pem",
|
||||
"cert_file": "consul-client-dc1-0.pem",
|
||||
"key_file": "consul-client-dc1-0-key.pem",
|
||||
"ports": {
|
||||
"http": -1,
|
||||
"https": 8501
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using `localhost` and `127.0.0.1` as subject alternate names (SANs) allows
|
||||
tools like `curl` to be able to communicate with Consul's HTTP API when run on
|
||||
the same host. Other SANs may be added including a DNS resolvable hostname to
|
||||
allow remote HTTP requests from third party tools.
|
||||
This configuration disables the HTTP port to make sure there is only encryted
|
||||
communication. Existing clients that are not yet prepared to talk HTTPS won't be
|
||||
able to connect afterwards. This also affects builtin tooling like `consul
|
||||
members` and the UI. The next chapters will demonstrate how to setup secure
|
||||
access.
|
||||
|
||||
You should now have the following files:
|
||||
After a Consul agent restart, your agents should be only talking TLS.
|
||||
|
||||
* `cfssl.json` - cfssl configuration.
|
||||
* `consul-ca.csr` - CA signing request.
|
||||
* `consul-ca-key.pem` - CA private key. Keep safe!
|
||||
* `consul-ca.pem` - CA public certificate.
|
||||
* `cli.csr` - Consul CLI certificate signing request.
|
||||
* `cli-key.pem` - Consul CLI private key.
|
||||
* `cli.pem` - Consul CLI certificate.
|
||||
* `client.csr` - Consul client node certificate signing request for the `global` region.
|
||||
* `client-key.pem` - Consul client node private key for the `global` region.
|
||||
* `client.pem` - Consul client node public certificate for the `global` region.
|
||||
* `server.csr` - Consul server node certificate signing request for the `global` region.
|
||||
* `server-key.pem` - Consul server node private key for the `global` region.
|
||||
* `server.pem` - Consul server node public certificate for the `global` region.
|
||||
## Configuring the Consul CLI for HTTPS
|
||||
|
||||
Each Consul node should have the appropriate key (`-key.pem`) and certificate
|
||||
(`.pem`) file for its region and role. In addition each node needs the CA's
|
||||
public certificate (`consul-ca.pem`).
|
||||
|
||||
Please note you will need the keys for the CLI if you choose to disable
|
||||
HTTP (in which case running the command `consul members` will return an error).
|
||||
This is because the Consul CLI defaults to communicating via HTTP instead of
|
||||
HTTPS. We can configure the local Consul client to connect using TLS and specify
|
||||
our custom keys and certificates using the command line:
|
||||
If your cluster is configured to only communicate via HTTPS, you will need to
|
||||
create additional certificates in order to be able to continue to access the API
|
||||
and the UI:
|
||||
|
||||
```shell
|
||||
$ consul members -ca-file=consul-ca.pem -client-cert=cli.pem -client-key=cli-key.pem -http-addr="https://localhost:9090"
|
||||
$ consul tls cert create -cli
|
||||
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
|
||||
==> Saved consul-cli-dc1-0.pem
|
||||
==> Saved consul-cli-dc1-0-key.pem
|
||||
```
|
||||
|
||||
If you are trying to get members of you cluster, the CLI will return an error:
|
||||
|
||||
```shell
|
||||
$ consul members
|
||||
Error retrieving members:
|
||||
Get http://127.0.0.1:8500/v1/agent/members?segment=_all:
|
||||
dial tcp 127.0.0.1:8500: connect: connection refused
|
||||
$ consul members -http-addr="https://localhost:8501"
|
||||
Error retrieving members:
|
||||
Get https://localhost:8501/v1/agent/members?segment=_all:
|
||||
x509: certificate signed by unknown authority
|
||||
```
|
||||
|
||||
But it will work again if you provide the certificates you provided:
|
||||
|
||||
```shell
|
||||
$ consul members -ca-file=consul-agent-ca.pem -client-cert=consul-cli-dc1-0.pem \
|
||||
-client-key=consul-cli-dc1-0-key.pem -http-addr="https://localhost:8501"
|
||||
Node Address Status Type Build Protocol DC Segment
|
||||
...
|
||||
```
|
||||
(The command is assuming HTTPS is configured to use port 9090. To see how
|
||||
you can change this, visit the [Configuration](/docs/agent/options.html) page)
|
||||
|
||||
This process can be cumbersome to type each time, so the Consul CLI also
|
||||
searches environment variables for default values. Set the following
|
||||
environment variables in your shell:
|
||||
|
||||
```shell
|
||||
$ export CONSUL_HTTP_ADDR=https://localhost:9090
|
||||
$ export CONSUL_CACERT=consul-ca.pem
|
||||
$ export CONSUL_CLIENT_CERT=cli.pem
|
||||
$ export CONSUL_CLIENT_KEY=cli-key.pem
|
||||
$ export CONSUL_HTTP_ADDR=https://localhost:8501
|
||||
$ export CONSUL_CACERT=consul-agent-ca.pem
|
||||
$ export CONSUL_CLIENT_CERT=consul-cli-dc1-0.pem
|
||||
$ export CONSUL_CLIENT_KEY=consul-cli-dc1-0-key.pem
|
||||
```
|
||||
|
||||
* `CONSUL_HTTP_ADDR` is the URL of the Consul agent and sets the default for
|
||||
|
@ -192,7 +276,159 @@ $ export CONSUL_CLIENT_KEY=cli-key.pem
|
|||
After these environment variables are correctly configured, the CLI will
|
||||
respond as expected.
|
||||
|
||||
[cfssl]: https://cfssl.org/
|
||||
### Note on SANs for Server and Client Certificates
|
||||
|
||||
Using `localhost` and `127.0.0.1` as `Subject Alternative Names` in server
|
||||
and client certificates allows tools like `curl` to be able to communicate with
|
||||
Consul's HTTPS API when run on the same host. Other SANs may be added during
|
||||
server/client certificates creation with `-additional-dnsname` to allow remote
|
||||
HTTPS requests from other hosts.
|
||||
|
||||
## Configuring the Consul UI for HTTPS
|
||||
|
||||
If your servers and clients are configured now like above, you won't be able to
|
||||
access the builtin UI anymore. We recommend that you pick one (or two for
|
||||
availability) Consul agent you want to run the UI on and follow the instructions
|
||||
to get the UI up and running again.
|
||||
|
||||
### Step 1: Which interface to bind to?
|
||||
|
||||
Depending on your setup you might need to change to which interface you are
|
||||
binding because thats `127.0.0.1` by default for the UI. Either via the
|
||||
[`addresses.https`](/docs/agent/options.html#https) or
|
||||
[client_addr](/docs/agent/options.html#client_addr) option which also impacts
|
||||
the DNS server. The Consul UI is unproteced which means you need to put some
|
||||
auth in front of it if you want to make it publicly available!
|
||||
|
||||
Binding to `0.0.0.0` should work:
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": true,
|
||||
"client_addr": "0.0.0.0",
|
||||
"enable_script_checks": false,
|
||||
"disable_remote_exec": true
|
||||
}
|
||||
```
|
||||
|
||||
~> Since your Consul agent is now available to the network, please make sure
|
||||
that [`enable_script_checks`](/docs/agent/options.html#_enable_script_checks) is
|
||||
set to `false` and
|
||||
[`disable_remote_exec`](https://www.consul.io/docs/agent/options.html#disable_remote_exec)
|
||||
is set to `true`.
|
||||
|
||||
### Step 2: verify_incoming_rpc
|
||||
|
||||
Your Consul agent will deny the connection straight away because
|
||||
`verify_incoming` is enabled.
|
||||
|
||||
> If set to true, Consul requires that all incoming connections make use of TLS
|
||||
> and that the client provides a certificate signed by a Certificate Authority
|
||||
> from the ca_file or ca_path. This applies to both server RPC and to the HTTPS
|
||||
> API.
|
||||
|
||||
Since the browser doesn't present a certificate signed by our CA, you cannot
|
||||
access the UI. If you `curl` your HTTPS UI the following happens:
|
||||
|
||||
```shell
|
||||
$ curl https://localhost:8501/ui/ -k -I
|
||||
curl: (35) error:14094412:SSL routines:SSL3_READ_BYTES:sslv3 alert bad certificate
|
||||
```
|
||||
|
||||
This is the Consul HTTPS server denying your connection because you are not
|
||||
presenting a client certificate signed by your Consul CA. There is a combination
|
||||
of options however that allows us to keep using `verify_incoming` for RPC, but
|
||||
not for HTTPS:
|
||||
|
||||
```json
|
||||
{
|
||||
"verify_incoming": false,
|
||||
"verify_incoming_rpc": true
|
||||
}
|
||||
```
|
||||
|
||||
~> This is the only time we are changing the value of the existing option
|
||||
`verify_incoming` to false. Make sure to only change it on the agent running the
|
||||
UI!
|
||||
|
||||
With the new configuration, it should work:
|
||||
|
||||
```shell
|
||||
$ curl https://localhost:8501/ui/ -k -I
|
||||
HTTP/2 200
|
||||
...
|
||||
```
|
||||
|
||||
### Step 3: Subject Alternative Name
|
||||
|
||||
This step will take care of setting up the domain you want to use to access the
|
||||
Consul UI. Unless you only need to access the UI over localhost or 127.0.0.1 you
|
||||
will need to go complete this step.
|
||||
|
||||
```shell
|
||||
$ curl https://consul.example.com:8501/ui/ \
|
||||
--resolve 'consul.example.com:8501:127.0.0.1' \
|
||||
--cacert consul-agent-ca.pem
|
||||
curl: (51) SSL: no alternative certificate subject name matches target host name 'consul.example.com'
|
||||
...
|
||||
```
|
||||
|
||||
The above command simulates a request a browser is making when you are trying to
|
||||
use the domain `consul.example.com` to access your UI. The problem this time is
|
||||
that your domain is not in `Subject Alternative Name` of the Certificate. We can
|
||||
fix that by creating a certificate that has our domain:
|
||||
|
||||
```shell
|
||||
$ consul tls cert create -server -additional-dnsname consul.example.com
|
||||
...
|
||||
```
|
||||
|
||||
And if you put your new cert into the configuration of the agent you picked to
|
||||
serve the UI and restart Consul, it works now:
|
||||
|
||||
```shell
|
||||
$ curl https://consul.example.com:8501/ui/ \
|
||||
--resolve 'consul.example.com:8501:127.0.0.1' \
|
||||
--cacert consul-agent-ca.pem -I
|
||||
HTTP/2 200
|
||||
...
|
||||
```
|
||||
|
||||
### Step 4: Trust the Consul CA
|
||||
|
||||
So far we have provided curl with our CA so that it can verify the connection,
|
||||
but if we stop doing that it will complain and so will our browser if you visit
|
||||
your UI on https://consul.example.com:
|
||||
|
||||
```shell
|
||||
$ curl https://consul.example.com:8501/ui/ \
|
||||
--resolve 'consul.example.com:8501:127.0.0.1'
|
||||
curl: (60) SSL certificate problem: unable to get local issuer certificate
|
||||
...
|
||||
```
|
||||
|
||||
You can fix that by trusting your Consul CA (`consul-agent-ca.pem`) on your machine,
|
||||
please use Google to find out how to do that on your OS.
|
||||
|
||||
```shell
|
||||
$ curl https://consul.example.com:8501/ui/ \
|
||||
--resolve 'consul.example.com:8501:127.0.0.1' -I
|
||||
HTTP/2 200
|
||||
...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
When you have completed this guide, your Consul cluster will have TLS enabled
|
||||
and will encrypt all RPC and HTTP traffic (assuming you disabled the HTTP port).
|
||||
The other pre-requisites for a secure Consul deployment are:
|
||||
|
||||
* [Enable gossip encryption](/docs/agent/encryption.html#gossip-encryption)
|
||||
* [Configure ACLs][acl] with default deny
|
||||
|
||||
[letsencrypt]: https://letsencrypt.org/
|
||||
[vault]: https://www.vaultproject.io/
|
||||
[vault-pki]: https://www.vaultproject.io/docs/secrets/pki/index.html
|
||||
[guide]: /docs/agent/encryption.html#configuring-tls-on-an-existing-cluster
|
||||
[acl]: /docs/guides/acl.html
|
||||
|
||||
|
|
|
@ -241,6 +241,18 @@
|
|||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-tls") %>>
|
||||
<a href="/docs/commands/tls.html">tls</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-commands-tls-ca") %>>
|
||||
<a href="/docs/commands/tls/ca.html">ca</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-commands-tls-cert") %>>
|
||||
<a href="/docs/commands/tls/cert.html">cert</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-validate") %>>
|
||||
<a href="/docs/commands/validate.html">validate</a>
|
||||
</li>
|
||||
|
@ -409,7 +421,7 @@
|
|||
<a href="/docs/guides/consul-aws.html">Consul-AWS</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-guides-creating-certificates") %>>
|
||||
<a href="/docs/guides/creating-certificates.html">Creating Certificates</a>
|
||||
<a href="/docs/guides/creating-certificates.html">Creating TLS Certificates</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-guides-deployment-guide") %>>
|
||||
<a href="/docs/guides/deployment-guide.html">Deployment Guide</a>
|
||||
|
|
Loading…
Reference in New Issue