From acc458d7a45d44ed60378b2afe16fc0d50997bbc Mon Sep 17 00:00:00 2001 From: Hans Hasselberg Date: Wed, 19 Dec 2018 09:22:49 +0100 Subject: [PATCH] Builtin tls helper (#5078) * command: add tls subcommand * website: update docs and guide --- command/commands_oss.go | 10 + command/connect/connect_test.go | 2 +- command/tls/ca/create/tls_ca_create.go | 113 ++++ command/tls/ca/create/tls_ca_create_test.go | 13 + command/tls/ca/tls_ca.go | 42 ++ command/tls/ca/tls_ca_test.go | 13 + command/tls/cert/create/tls_cert_create.go | 216 ++++++++ .../tls/cert/create/tls_cert_create_test.go | 13 + command/tls/cert/tls_cert.go | 48 ++ command/tls/cert/tls_cert_test.go | 13 + command/tls/generate.go | 216 ++++++++ command/tls/generate_test.go | 146 +++++ command/tls/tls.go | 57 ++ command/tls/tls_test.go | 13 + website/source/docs/commands/tls.html.md | 53 ++ .../source/docs/commands/tls/ca.html.md.erb | 28 + .../source/docs/commands/tls/cert.html.md.erb | 68 +++ .../docs/guides/creating-certificates.html.md | 508 +++++++++++++----- website/source/layouts/docs.erb | 14 +- 19 files changed, 1448 insertions(+), 138 deletions(-) create mode 100644 command/tls/ca/create/tls_ca_create.go create mode 100644 command/tls/ca/create/tls_ca_create_test.go create mode 100644 command/tls/ca/tls_ca.go create mode 100644 command/tls/ca/tls_ca_test.go create mode 100644 command/tls/cert/create/tls_cert_create.go create mode 100644 command/tls/cert/create/tls_cert_create_test.go create mode 100644 command/tls/cert/tls_cert.go create mode 100644 command/tls/cert/tls_cert_test.go create mode 100644 command/tls/generate.go create mode 100644 command/tls/generate_test.go create mode 100644 command/tls/tls.go create mode 100644 command/tls/tls_test.go create mode 100644 website/source/docs/commands/tls.html.md create mode 100644 website/source/docs/commands/tls/ca.html.md.erb create mode 100644 website/source/docs/commands/tls/cert.html.md.erb diff --git a/command/commands_oss.go b/command/commands_oss.go index 324d8f27ea..91fee14397 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -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 }) diff --git a/command/connect/connect_test.go b/command/connect/connect_test.go index 95c8ebd58e..3f78c2b7a6 100644 --- a/command/connect/connect_test.go +++ b/command/connect/connect_test.go @@ -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") diff --git a/command/tls/ca/create/tls_ca_create.go b/command/tls/ca/create/tls_ca_create.go new file mode 100644 index 0000000000..bbe54a89a4 --- /dev/null +++ b/command/tls/ca/create/tls_ca_create.go @@ -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 +` diff --git a/command/tls/ca/create/tls_ca_create_test.go b/command/tls/ca/create/tls_ca_create_test.go new file mode 100644 index 0000000000..f1f96e8916 --- /dev/null +++ b/command/tls/ca/create/tls_ca_create_test.go @@ -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") + } +} diff --git a/command/tls/ca/tls_ca.go b/command/tls/ca/tls_ca.go new file mode 100644 index 0000000000..bca562bb97 --- /dev/null +++ b/command/tls/ca/tls_ca.go @@ -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 [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. +` diff --git a/command/tls/ca/tls_ca_test.go b/command/tls/ca/tls_ca_test.go new file mode 100644 index 0000000000..eee5f40091 --- /dev/null +++ b/command/tls/ca/tls_ca_test.go @@ -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") + } +} diff --git a/command/tls/cert/create/tls_cert_create.go b/command/tls/cert/create/tls_cert_create.go new file mode 100644 index 0000000000..f15c632994 --- /dev/null +++ b/command/tls/cert/create/tls_cert_create.go @@ -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 +` diff --git a/command/tls/cert/create/tls_cert_create_test.go b/command/tls/cert/create/tls_cert_create_test.go new file mode 100644 index 0000000000..f1f96e8916 --- /dev/null +++ b/command/tls/cert/create/tls_cert_create_test.go @@ -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") + } +} diff --git a/command/tls/cert/tls_cert.go b/command/tls/cert/tls_cert.go new file mode 100644 index 0000000000..57514ad876 --- /dev/null +++ b/command/tls/cert/tls_cert.go @@ -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 [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. +` diff --git a/command/tls/cert/tls_cert_test.go b/command/tls/cert/tls_cert_test.go new file mode 100644 index 0000000000..958c7f76fa --- /dev/null +++ b/command/tls/cert/tls_cert_test.go @@ -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") + } +} diff --git a/command/tls/generate.go b/command/tls/generate.go new file mode 100644 index 0000000000..b51ecef59c --- /dev/null +++ b/command/tls/generate.go @@ -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 +} diff --git a/command/tls/generate_test.go b/command/tls/generate_test.go new file mode 100644 index 0000000000..2e37e48034 --- /dev/null +++ b/command/tls/generate_test.go @@ -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])) +} diff --git a/command/tls/tls.go b/command/tls/tls.go new file mode 100644 index 0000000000..c3a5e35451 --- /dev/null +++ b/command/tls/tls.go @@ -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 [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. +` diff --git a/command/tls/tls_test.go b/command/tls/tls_test.go new file mode 100644 index 0000000000..ccf8c22ac0 --- /dev/null +++ b/command/tls/tls_test.go @@ -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") + } +} diff --git a/website/source/docs/commands/tls.html.md b/website/source/docs/commands/tls.html.md new file mode 100644 index 0000000000..786d64631d --- /dev/null +++ b/website/source/docs/commands/tls.html.md @@ -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 [options]` + +For the exact documentation for your Consul version, run `consul tls -h` to +view the complete list of subcommands. + +```text +Usage: consul tls [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: diff --git a/website/source/docs/commands/tls/ca.html.md.erb b/website/source/docs/commands/tls/ca.html.md.erb new file mode 100644 index 0000000000..73edf126b8 --- /dev/null +++ b/website/source/docs/commands/tls/ca.html.md.erb @@ -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=` - Provide number of days the CA is valid for from now on, defaults to 5 years. \ No newline at end of file diff --git a/website/source/docs/commands/tls/cert.html.md.erb b/website/source/docs/commands/tls/cert.html.md.erb new file mode 100644 index 0000000000..924375024f --- /dev/null +++ b/website/source/docs/commands/tls/cert.html.md.erb @@ -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=` - Provide additional dnsname for Subject Alternative Names. + +- `-ca=` - Provide path to the ca + +- `-cli` - Generate cli certificate + +- `-client` - Generate client certificate + +- `-days=` - Provide number of days the certificate is valid for from now on. + +- `-dc=` - Provide the datacenter. Matters only for -server certificates + +- `-domain=` - Provide the domain. Matters only for -server certificates + +- `-key=` - Provide path to the key + +- `-server` - Generate server certificate \ No newline at end of file diff --git a/website/source/docs/guides/creating-certificates.html.md b/website/source/docs/guides/creating-certificates.html.md index 6f038c9d96..759c150806 100644 --- a/website/source/docs/guides/creating-certificates.html.md +++ b/website/source/docs/guides/creating-certificates.html.md @@ -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 + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 670f49f7a4..cc6dc2b135 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -241,6 +241,18 @@ + > + tls + + + > validate @@ -409,7 +421,7 @@ Consul-AWS > - Creating Certificates + Creating TLS Certificates > Deployment Guide