From 0263e7af34d027ecc255974800958eaa787f963d Mon Sep 17 00:00:00 2001 From: Lance Haig Date: Tue, 22 Nov 2022 20:12:07 +0100 Subject: [PATCH] Add command "nomad tls" (#14296) --- .changelog/14296.txt | 4 + command/commands.go | 35 +++ command/tls.go | 56 ++++ command/tls_ca.go | 44 +++ command/tls_ca_create.go | 162 ++++++++++ command/tls_ca_create_test.go | 98 ++++++ command/tls_ca_info.go | 91 ++++++ command/tls_cert.go | 44 +++ command/tls_cert_create.go | 293 ++++++++++++++++++ command/tls_cert_create_test.go | 194 ++++++++++++ command/tls_cert_info.go | 91 ++++++ helper/tlsutil/config.go | 4 +- helper/tlsutil/config_test.go | 11 +- helper/tlsutil/generate.go | 17 +- helper/tlsutil/generate_test.go | 6 +- lib/file/atomic.go | 51 +++ testutil/tls.go | 49 +++ .../content/docs/commands/tls/ca-create.mdx | 59 ++++ website/content/docs/commands/tls/ca-info.mdx | 42 +++ .../content/docs/commands/tls/cert-create.mdx | 86 +++++ .../content/docs/commands/tls/cert-info.mdx | 32 ++ website/content/docs/commands/tls/index.mdx | 28 ++ website/data/docs-nav-data.json | 25 ++ 23 files changed, 1510 insertions(+), 12 deletions(-) create mode 100644 .changelog/14296.txt create mode 100644 command/tls.go create mode 100644 command/tls_ca.go create mode 100644 command/tls_ca_create.go create mode 100644 command/tls_ca_create_test.go create mode 100644 command/tls_ca_info.go create mode 100644 command/tls_cert.go create mode 100644 command/tls_cert_create.go create mode 100644 command/tls_cert_create_test.go create mode 100644 command/tls_cert_info.go create mode 100644 lib/file/atomic.go create mode 100644 testutil/tls.go create mode 100644 website/content/docs/commands/tls/ca-create.mdx create mode 100644 website/content/docs/commands/tls/ca-info.mdx create mode 100644 website/content/docs/commands/tls/cert-create.mdx create mode 100644 website/content/docs/commands/tls/cert-info.mdx create mode 100644 website/content/docs/commands/tls/index.mdx diff --git a/.changelog/14296.txt b/.changelog/14296.txt new file mode 100644 index 000000000..cd2325e28 --- /dev/null +++ b/.changelog/14296.txt @@ -0,0 +1,4 @@ +```release-note:improvement +cli: Added tls command to enable creating Certificate Authority and Self signed TLS certificates. +There are two sub commands `tls ca` and `tls cert` that are helpers when creating certificates. +``` \ No newline at end of file diff --git a/command/commands.go b/command/commands.go index e65914ed9..c5ad24ce8 100644 --- a/command/commands.go +++ b/command/commands.go @@ -926,6 +926,41 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "tls": func() (cli.Command, error) { + return &TLSCommand{ + Meta: meta, + }, nil + }, + "tls ca": func() (cli.Command, error) { + return &TLSCACommand{ + Meta: meta, + }, nil + }, + "tls ca create": func() (cli.Command, error) { + return &TLSCACreateCommand{ + Meta: meta, + }, nil + }, + "tls ca info": func() (cli.Command, error) { + return &TLSCAInfoCommand{ + Meta: meta, + }, nil + }, + "tls cert": func() (cli.Command, error) { + return &TLSCertCommand{ + Meta: meta, + }, nil + }, + "tls cert create": func() (cli.Command, error) { + return &TLSCertCreateCommand{ + Meta: meta, + }, nil + }, + "tls cert info": func() (cli.Command, error) { + return &TLSCertInfoCommand{ + Meta: meta, + }, nil + }, "ui": func() (cli.Command, error) { return &UiCommand{ Meta: meta, diff --git a/command/tls.go b/command/tls.go new file mode 100644 index 000000000..c9bab3260 --- /dev/null +++ b/command/tls.go @@ -0,0 +1,56 @@ +package command + +import ( + "os" + "strings" + + "github.com/mitchellh/cli" +) + +type TLSCommand struct { + Meta +} + +func fileDoesNotExist(file string) bool { + if _, err := os.Stat(file); os.IsNotExist(err) { + return true + } + return false +} + +func (c *TLSCommand) Help() string { + helpText := ` +Usage: nomad tls [options] + +This command groups subcommands for creating certificates for Nomad TLS configuration. +The TLS command allows operators to generate self signed certificates to use +when securing your Nomad cluster. + +Some simple examples for creating certificates can be found here. +More detailed examples are available in the subcommands or the documentation. + +Create a CA + + $ nomad tls ca create + +Create a server certificate + + $ nomad tls cert create -server + +Create a client certificate + + $ nomad tls cert create -client + +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCommand) Synopsis() string { + return "Generate Self Signed TLS Certificates for Nomad" +} + +func (c *TLSCommand) Name() string { return "tls" } + +func (c *TLSCommand) Run(_ []string) int { + return cli.RunResultHelp +} diff --git a/command/tls_ca.go b/command/tls_ca.go new file mode 100644 index 000000000..9bb67504c --- /dev/null +++ b/command/tls_ca.go @@ -0,0 +1,44 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +type TLSCACommand struct { + Meta +} + +func (c *TLSCACommand) Help() string { + helpText := ` +Usage: nomad tls ca [options] + + This command groups subcommands for interacting with certificate authorities. + For examples, see the documentation. + + Create a certificate authority. + + $ nomad tls ca create + + Show information about a certificate authority. + + $ nomad tls ca info +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCACommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *TLSCACommand) Synopsis() string { + return "Helpers for managing certificate authorities" +} + +func (c *TLSCACommand) Name() string { return "tls ca" } + +func (c *TLSCACommand) Run(_ []string) int { + return cli.RunResultHelp +} diff --git a/command/tls_ca_create.go b/command/tls_ca_create.go new file mode 100644 index 000000000..9ceda2645 --- /dev/null +++ b/command/tls_ca_create.go @@ -0,0 +1,162 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/posener/complete" + + "github.com/hashicorp/nomad/helper/flags" + "github.com/hashicorp/nomad/helper/tlsutil" + "github.com/hashicorp/nomad/lib/file" +) + +type TLSCACreateCommand struct { + Meta + + // days is the number of days the CA will be valid for + days int + + // constraint boolean enables the name constraint option in the CA which + // will then reject any domains other than the ones stiputalted in -domain + // and -addtitional-domain. + constraint bool + + // domain is used to provide a custom domain for the CA + domain string + + // commonName is used to set a common name for the CA + commonName string + + // additionalDomain provides a list of restricted domains to the CA which + // will then reject any domains other than these. + additionalDomain flags.StringFlag +} + +func (c *TLSCACreateCommand) Help() string { + helpText := ` +Usage: nomad tls ca create [options] + + Create a new certificate authority. + +CA Create Options: + + -additional-domain + Add additional DNS zones to the allowed list for the CA. The server will + reject certificates for DNS names other than those specified in -domain and + -additional-domain. This flag can be used multiple times. Only used in + combination with -domain and -name-constraint. + + -common-name + Common Name of CA. Defaults to "Nomad Agent CA". + + -days + Provide number of days the CA is valid for from now on. + Defaults to 5 years or 1825 days. + + -domain + Domain of Nomad cluster. Only used in combination with -name-constraint. + Defaults to "nomad". + + -name-constraint + Enables the DNS name restriction functionality to the CA. Results in the CA + rejecting certificates for any other DNS zone. If enabled, localhost and the + value of -domain will be added to the allowed DNS zones field. If the UI is + going to be served over HTTPS its hostname must be added with + -additional-domain. Defaults to false. +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCACreateCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-additional-domain": complete.PredictAnything, + "-common-name": complete.PredictAnything, + "-days": complete.PredictAnything, + "-domain": complete.PredictAnything, + "-name-constraint": complete.PredictAnything, + }) +} + +func (c *TLSCACreateCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *TLSCACreateCommand) Synopsis() string { + return "Create a certificate authority for Nomad" +} + +func (c *TLSCACreateCommand) Name() string { return "tls ca create" } + +func (c *TLSCACreateCommand) Run(args []string) int { + + flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) + flagSet.Usage = func() { c.Ui.Output(c.Help()) } + flagSet.Var(&c.additionalDomain, "additional-domain", "") + flagSet.IntVar(&c.days, "days", 1825, "") + flagSet.BoolVar(&c.constraint, "name-constraint", false, "") + flagSet.StringVar(&c.domain, "domain", "nomad", "") + flagSet.StringVar(&c.commonName, "common-name", "", "") + if err := flagSet.Parse(args); err != nil { + return 1 + } + + // Check that we got no arguments + args = flagSet.Args() + if l := len(args); l < 0 || l > 1 { + c.Ui.Error("This command takes up to one argument") + c.Ui.Error(commandErrorText(c)) + return 1 + } + if c.domain != "" && c.domain != "nomad" && !c.constraint { + c.Ui.Error("Please provide the -name-constraint flag to use a custom domain constraint") + return 1 + } + if c.domain == "nomad" && c.constraint { + c.Ui.Error("Please provide the -domain flag if you want to enable custom domain constraints") + return 1 + } + if c.additionalDomain != nil && c.domain == "" && !c.constraint { + c.Ui.Error("Please provide the -name-constraint flag to use a custom domain constraints") + return 1 + } + + certFileName := fmt.Sprintf("%s-agent-ca.pem", c.domain) + pkFileName := fmt.Sprintf("%s-agent-ca-key.pem", c.domain) + + if !(fileDoesNotExist(certFileName)) { + c.Ui.Error(fmt.Sprintf("CA certificate file '%s' already exists", certFileName)) + return 1 + } + if !(fileDoesNotExist(pkFileName)) { + c.Ui.Error(fmt.Sprintf("CA key file '%s' already exists", pkFileName)) + return 1 + } + + constraints := []string{} + if c.constraint { + constraints = []string{c.domain, "localhost"} + constraints = append(constraints, c.additionalDomain...) + } + + ca, pk, err := tlsutil.GenerateCA(tlsutil.CAOpts{Name: c.commonName, Days: c.days, Domain: c.domain, PermittedDNSDomains: constraints}) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err := file.WriteAtomicWithPerms(certFileName, []byte(ca), 0755, 0666); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + c.Ui.Output("==> CA certificate saved to: " + certFileName) + + if err := file.WriteAtomicWithPerms(pkFileName, []byte(pk), 0755, 0600); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + c.Ui.Output("==> CA certificate key saved to: " + pkFileName) + + return 0 +} diff --git a/command/tls_ca_create_test.go b/command/tls_ca_create_test.go new file mode 100644 index 000000000..aee19663a --- /dev/null +++ b/command/tls_ca_create_test.go @@ -0,0 +1,98 @@ +package command + +import ( + "crypto/x509" + "os" + "strings" + "testing" + "time" + + "github.com/hashicorp/nomad/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestCACreateCommand(t *testing.T) { + testDir := t.TempDir() + previousDirectory, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(testDir)) + defer os.Chdir(previousDirectory) + + type testcase struct { + name string + args []string + caPath string + keyPath string + extraCheck func(t *testing.T, cert *x509.Certificate) + } + // The following subtests must run serially. + cases := []testcase{ + {"ca defaults", + nil, + "nomad-agent-ca.pem", + "nomad-agent-ca-key.pem", + func(t *testing.T, cert *x509.Certificate) { + require.Equal(t, 1825*24*time.Hour, time.Until(cert.NotAfter).Round(24*time.Hour)) + require.False(t, cert.PermittedDNSDomainsCritical) + require.Len(t, cert.PermittedDNSDomains, 0) + }, + }, + {"ca options", + []string{ + "-days=365", + "-name-constraint=true", + "-domain=foo", + "-additional-domain=bar", + }, + "foo-agent-ca.pem", + "foo-agent-ca-key.pem", + func(t *testing.T, cert *x509.Certificate) { + require.Equal(t, 365*24*time.Hour, time.Until(cert.NotAfter).Round(24*time.Hour)) + require.True(t, cert.PermittedDNSDomainsCritical) + require.Len(t, cert.PermittedDNSDomains, 3) + require.ElementsMatch(t, cert.PermittedDNSDomains, []string{"foo", "localhost", "bar"}) + }, + }, + {"with common-name", + []string{ + "-common-name=foo", + }, + "nomad-agent-ca.pem", + "nomad-agent-ca-key.pem", + func(t *testing.T, cert *x509.Certificate) { + require.Equal(t, cert.Subject.CommonName, "foo") + }, + }, + {"without common-name", + []string{}, + "nomad-agent-ca.pem", + "nomad-agent-ca-key.pem", + func(t *testing.T, cert *x509.Certificate) { + require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Nomad Agent CA")) + }, + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &TLSCACreateCommand{Meta: Meta{Ui: ui}} + require.Equal(t, 0, cmd.Run(tc.args), ui.ErrorWriter.String()) + require.Equal(t, "", ui.ErrorWriter.String()) + // is a valid key + key := testutil.IsValidSigner(t, tc.keyPath) + require.True(t, key) + // is a valid ca expects the ca + ca := testutil.IsValidCertificate(t, tc.caPath) + require.True(t, ca.BasicConstraintsValid) + require.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageDigitalSignature, ca.KeyUsage) + require.True(t, ca.IsCA) + require.Equal(t, ca.AuthorityKeyId, ca.SubjectKeyId) + tc.extraCheck(t, ca) + require.NoError(t, os.Remove(tc.caPath)) + require.NoError(t, os.Remove(tc.keyPath)) + }) + } + +} diff --git a/command/tls_ca_info.go b/command/tls_ca_info.go new file mode 100644 index 000000000..0c331511e --- /dev/null +++ b/command/tls_ca_info.go @@ -0,0 +1,91 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/nomad/helper/tlsutil" + "github.com/posener/complete" + "github.com/ryanuber/columnize" +) + +type TLSCAInfoCommand struct { + Meta +} + +func (c *TLSCAInfoCommand) Help() string { + helpText := ` +Usage: nomad tls ca info + + Show information about a certificate authority. +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCAInfoCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{}) +} + +func (c *TLSCAInfoCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictOr( + complete.PredictFiles("*.pem"), + ) +} + +func (c *TLSCAInfoCommand) Synopsis() string { + return "Show certificate authority information" +} + +func (c *TLSCAInfoCommand) Name() string { return "tls cert info" } + +func (c *TLSCAInfoCommand) Run(args []string) int { + + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got no arguments + args = flags.Args() + if l := len(args); l < 0 || l > 1 { + c.Ui.Error("This command takes up to one argument") + c.Ui.Error(commandErrorText(c)) + return 1 + } + var certFile []byte + var err error + var file string + if len(args) == 0 { + c.Ui.Error(fmt.Sprintf("Error reading CA file: %v", err)) + return 1 + } + if len(args) == 1 { + file = args[0] + certFile, err = os.ReadFile(file) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading CA file: %v", err)) + return 1 + } + } + + certInfo, err := tlsutil.ParseCert(string(certFile)) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + // Format the certificate info + basic := []string{ + fmt.Sprintf("Serial Number|%s", certInfo.SerialNumber), + fmt.Sprintf("Issuer CN|%s", certInfo.Issuer.CommonName), + fmt.Sprintf("Common Name|%s", certInfo.Subject), + fmt.Sprintf("Expiry Date|%s", certInfo.NotAfter), + fmt.Sprintf("Permitted DNS Domains|%s", certInfo.PermittedDNSDomains), + } + + // Print out the information + c.Ui.Output(columnize.SimpleFormat(basic)) + return 0 +} diff --git a/command/tls_cert.go b/command/tls_cert.go new file mode 100644 index 000000000..9b7cecf13 --- /dev/null +++ b/command/tls_cert.go @@ -0,0 +1,44 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +type TLSCertCommand struct { + Meta +} + +func (c *TLSCertCommand) Help() string { + helpText := ` +Usage: nomad tls cert [options] + + This command groups subcommands for interacting with certificates. + For examples, see the documentation. + + Create a TLS certificate. + + $ nomad tls cert create + + Show information about a TLS certificate. + + $ nomad tls cert info +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCertCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *TLSCertCommand) Synopsis() string { + return "Helpers for managing certificates" +} + +func (c *TLSCertCommand) Name() string { return "tls cert" } + +func (c *TLSCertCommand) Run(_ []string) int { + return cli.RunResultHelp +} diff --git a/command/tls_cert_create.go b/command/tls_cert_create.go new file mode 100644 index 000000000..611c63b45 --- /dev/null +++ b/command/tls_cert_create.go @@ -0,0 +1,293 @@ +package command + +import ( + "crypto/x509" + "fmt" + "net" + "os" + "strings" + + "github.com/posener/complete" + + "github.com/hashicorp/nomad/helper/flags" + "github.com/hashicorp/nomad/helper/tlsutil" + "github.com/hashicorp/nomad/lib/file" +) + +type TLSCertCreateCommand struct { + Meta + + // dnsNames is a list of additional dns records to add to the SAN addresses + dnsNames flags.StringFlag + + // ipAddresses is a list of additional IP address records to add to the SAN + // addresses + ipAddresses flags.StringFlag + + // ca is used to set a custom CA certificate to create certificates from. + ca string + + cli bool + client bool + + // key is used to set the custom CA certificate key when creating + // certificates. + key string + + // days is the number of days the certificate will be valid for. + days int + + // cluster_region is used to add the region name to the certifacte SAN + // records + cluster_region string + + // domain is used to provide a custom domain for the certificate. + domain string + + server bool +} + +func (c *TLSCertCreateCommand) Help() string { + helpText := ` +Usage: nomad tls cert create [options] + + Create a new TLS certificate to use within the Nomad cluster TLS + configuration. You should use the -client, -server or -cli options to create + certificates for these roles. + +Certificate Create Options: + + -additional-dnsname + Provide an additional dnsname for Subject Alternative Names. + "localhost" is always included. This flag may be provided multiple times. + + -additional-ipaddress + Provide an additional ipaddress for Subject Alternative Names. + "127.0.0.1" is always included. This flag may be provided multiple times. + + -ca + Provide path to the certificate authority certificate. Defaults to + #DOMAIN#-agent-ca.pem. + + -cli + Generate a certificate for use with the Nomad CLI. + + -client + Generate a client certificate. + + -cluster-region + Provide the datacenter. Only used for -server certificates. + Defaults to "global". + + -days + Provide number of days the certificate is valid for from now on. + Defaults to 1 year. + + -domain + Provide the domain. Only used for -server certificates. + + -key + Provide path to the certificate authority key. Defaults to + #DOMAIN#-agent-ca-key.pem. + + -server + Generate a server certificate. +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCertCreateCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-additional-dnsname": complete.PredictAnything, + "-additional-ipaddress": complete.PredictAnything, + "-ca": complete.PredictAnything, + "-cli": complete.PredictNothing, + "-client": complete.PredictNothing, + "-days": complete.PredictAnything, + "-cluster-region": complete.PredictAnything, + "-domain": complete.PredictAnything, + "-key": complete.PredictAnything, + "-server": complete.PredictNothing, + }) +} + +func (c *TLSCertCreateCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *TLSCertCreateCommand) Synopsis() string { + return "Create a new TLS certificate" +} + +func (c *TLSCertCreateCommand) Name() string { return "tls cert create" } + +func (c *TLSCertCreateCommand) Run(args []string) int { + + flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) + flagSet.Usage = func() { c.Ui.Output(c.Help()) } + flagSet.Var(&c.dnsNames, "additional-dnsname", "") + flagSet.Var(&c.ipAddresses, "additional-ipaddress", "") + flagSet.StringVar(&c.ca, "ca", "#DOMAIN#-agent-ca.pem", "") + flagSet.BoolVar(&c.cli, "cli", false, "") + flagSet.BoolVar(&c.client, "client", false, "") + flagSet.StringVar(&c.key, "key", "#DOMAIN#-agent-ca-key.pem", "") + flagSet.IntVar(&c.days, "days", 365, "") + flagSet.StringVar(&c.cluster_region, "cluster-region", "global", "") + flagSet.StringVar(&c.domain, "domain", "nomad", "") + flagSet.BoolVar(&c.server, "server", false, "") + if err := flagSet.Parse(args); err != nil { + return 1 + } + + // Check that we got no arguments + args = flagSet.Args() + if l := len(args); l < 0 || l > 1 { + c.Ui.Error("This command takes up to one argument") + c.Ui.Error(commandErrorText(c)) + 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)) + } + } + + for _, i := range c.ipAddresses { + if len(i) > 0 { + IPAddresses = append(IPAddresses, net.ParseIP(strings.TrimSpace(i))) + } + } + + if c.server { + name = fmt.Sprintf("server.%s.%s", c.cluster_region, c.domain) + DNSNames = append(DNSNames, name) + DNSNames = append(DNSNames, "localhost") + + IPAddresses = append(IPAddresses, net.ParseIP("127.0.0.1")) + extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} + prefix = fmt.Sprintf("%s-server-%s", c.cluster_region, c.domain) + + } else if c.client { + name = fmt.Sprintf("client.%s.%s", c.cluster_region, c.domain) + DNSNames = append(DNSNames, []string{name, "localhost"}...) + IPAddresses = append(IPAddresses, net.ParseIP("127.0.0.1")) + extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + prefix = fmt.Sprintf("%s-client-%s", c.cluster_region, c.domain) + } else if c.cli { + name = fmt.Sprintf("cli.%s.%s", c.cluster_region, c.domain) + DNSNames = []string{name, "localhost"} + prefix = fmt.Sprintf("%s-cli-%s", c.cluster_region, c.domain) + } else { + c.Ui.Error("Neither client, cli nor server - should not happen") + return 1 + } + + var pkFileName, certFileName string + + tmpCert := fmt.Sprintf("%s.pem", prefix) + tmpPk := fmt.Sprintf("%s-key.pem", prefix) + + // Check if the CA file already exists + if !(fileDoesNotExist(tmpCert)) { + c.Ui.Error(fmt.Sprintf("Certificate file '%s' already exists", tmpCert)) + return 1 + } + // Check if the Key file file already exists + if !(fileDoesNotExist(tmpPk)) { + c.Ui.Error(fmt.Sprintf("Key file '%s' already exists", tmpPk)) + return 1 + } + + certFileName = tmpCert + pkFileName = tmpPk + + caFile := strings.Replace(c.ca, "#DOMAIN#", c.domain, 1) + keyFile := strings.Replace(c.key, "#DOMAIN#", c.domain, 1) + cert, err := os.ReadFile(caFile) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading CA: %s", err)) + return 1 + } + caKey, err := os.ReadFile(keyFile) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading CA key: %s", err)) + return 1 + } + + if c.server { + c.Ui.Warn( + `==> 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 CA file " + caFile + " and CA key " + keyFile) + + signer, err := tlsutil.ParseSigner(string(caKey)) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + pub, priv, err := tlsutil.GenerateCert(tlsutil.CertOpts{ + Signer: signer, CA: string(cert), Name: name, Days: c.days, + DNSNames: DNSNames, IPAddresses: IPAddresses, ExtKeyUsage: extKeyUsage, + }) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err = tlsutil.Verify(string(cert), pub, name); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err := file.WriteAtomicWithPerms(certFileName, []byte(pub), 0755, 0666); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if c.server { + c.Ui.Output("==> Server Certificate saved to " + certFileName) + } else if c.client { + c.Ui.Output("==> Client Certificate saved to " + certFileName) + } else if c.cli { + c.Ui.Output("==> Cli Certificate saved to " + certFileName) + } + + if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0600); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if c.server { + c.Ui.Output("==> Server Certificate key saved to " + pkFileName) + } else if c.client { + c.Ui.Output("==> Client Certificate key saved to " + pkFileName) + } else if c.cli { + c.Ui.Output("==> CLI Certificate key saved to " + pkFileName) + } + + return 0 +} diff --git a/command/tls_cert_create_test.go b/command/tls_cert_create_test.go new file mode 100644 index 000000000..ef7cfefc5 --- /dev/null +++ b/command/tls_cert_create_test.go @@ -0,0 +1,194 @@ +package command + +import ( + "crypto/x509" + "net" + "os" + "testing" + + "github.com/hashicorp/nomad/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestTlsCertCreateCommand_InvalidArgs(t *testing.T) { + t.Parallel() + + type testcase struct { + args []string + expectErr string + } + + cases := map[string]testcase{ + "no args (ca/key inferred)": {[]string{}, + "Please provide either -server, -client, or -cli"}, + "no ca": {[]string{"-ca", "", "-key", ""}, + "Please provide the ca"}, + "no key": {[]string{"-ca", "foo.pem", "-key", ""}, + "Please provide the key"}, + + "server+client+cli": {[]string{"-server", "-client", "-cli"}, + "Please provide either -server, -client, or -cli"}, + "server+client": {[]string{"-server", "-client"}, + "Please provide either -server, -client, or -cli"}, + "server+cli": {[]string{"-server", "-cli"}, + "Please provide either -server, -client, or -cli"}, + "client+cli": {[]string{"-client", "-cli"}, + "Please provide either -server, -client, or -cli"}, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + ui := cli.NewMockUi() + cmd := &TLSCertCreateCommand{Meta: Meta{Ui: ui}} + require.NotEqual(t, 0, cmd.Run(tc.args)) + got := ui.ErrorWriter.String() + if tc.expectErr == "" { + require.NotEmpty(t, got) // don't care + } else { + require.Contains(t, got, tc.expectErr) + } + }) + } +} + +func TestTlsCertCreateCommand_fileCreate(t *testing.T) { + testDir := t.TempDir() + previousDirectory, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(testDir)) + defer os.Chdir(previousDirectory) + + ui := cli.NewMockUi() + caCmd := &TLSCACreateCommand{Meta: Meta{Ui: ui}} + + // Setup CA keys + caCmd.Run([]string{"nomad"}) + + type testcase struct { + name string + typ string + args []string + certPath string + keyPath string + expectCN string + expectDNS []string + expectIP []net.IP + errOut string + } + + // The following subtests must run serially. + cases := []testcase{ + {"server0", + "server", + []string{"-server"}, + "global-server-nomad.pem", + "global-server-nomad-key.pem", + "server.global.nomad", + []string{ + "server.global.nomad", + "localhost", + }, + []net.IP{{127, 0, 0, 1}}, + "==> WARNING: Server Certificates grants authority to become a\n server and access all state in the cluster including root keys\n and all ACL tokens. Do not distribute them to production hosts\n that are not server nodes. Store them as securely as CA keys.\n", + }, + {"server0-region2-altdomain", + "server", + []string{"-server", "-cluster-region", "region2", "-domain", "nomad"}, + "region2-server-nomad.pem", + "region2-server-nomad-key.pem", + "server.region2.nomad", + []string{ + "server.region2.nomad", + "localhost", + }, + []net.IP{{127, 0, 0, 1}}, + "==> WARNING: Server Certificates grants authority to become a\n server and access all state in the cluster including root keys\n and all ACL tokens. Do not distribute them to production hosts\n that are not server nodes. Store them as securely as CA keys.\n", + }, + {"client0", + "client", + []string{"-client"}, + "global-client-nomad.pem", + "global-client-nomad-key.pem", + "client.global.nomad", + []string{ + "client.global.nomad", + "localhost", + }, + []net.IP{{127, 0, 0, 1}}, + "", + }, + {"client0-region2-altdomain", + "client", + []string{"-client", "-cluster-region", "region2", "-domain", "nomad"}, + "region2-client-nomad.pem", + "region2-client-nomad-key.pem", + "client.region2.nomad", + []string{ + "client.region2.nomad", + "localhost", + }, + []net.IP{{127, 0, 0, 1}}, + "", + }, + {"cli0", + "cli", + []string{"-cli"}, + "global-cli-nomad.pem", + "global-cli-nomad-key.pem", + "cli.global.nomad", + []string{ + "cli.global.nomad", + "localhost", + }, + nil, + "", + }, + {"cli0-region2-altdomain", + "cli", + []string{"-cli", "-cluster-region", "region2", "-domain", "nomad"}, + "region2-cli-nomad.pem", + "region2-cli-nomad-key.pem", + "cli.region2.nomad", + []string{ + "cli.region2.nomad", + "localhost", + }, + nil, + "", + }, + } + + for _, tc := range cases { + tc := tc + require.True(t, t.Run(tc.name, func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &TLSCertCreateCommand{Meta: Meta{Ui: ui}} + require.Equal(t, 0, cmd.Run(tc.args)) + require.Equal(t, tc.errOut, ui.ErrorWriter.String()) + + // is a valid cert expects the cert + cert := testutil.IsValidCertificate(t, tc.certPath) + require.Equal(t, tc.expectCN, cert.Subject.CommonName) + require.True(t, cert.BasicConstraintsValid) + require.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, cert.KeyUsage) + switch tc.typ { + case "server": + require.Equal(t, + []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + cert.ExtKeyUsage) + case "client": + require.Equal(t, + []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + cert.ExtKeyUsage) + case "cli": + require.Len(t, cert.ExtKeyUsage, 0) + } + require.False(t, cert.IsCA) + require.Equal(t, tc.expectDNS, cert.DNSNames) + require.Equal(t, tc.expectIP, cert.IPAddresses) + })) + } +} diff --git a/command/tls_cert_info.go b/command/tls_cert_info.go new file mode 100644 index 000000000..202509992 --- /dev/null +++ b/command/tls_cert_info.go @@ -0,0 +1,91 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/posener/complete" + "github.com/ryanuber/columnize" + + "github.com/hashicorp/nomad/helper/tlsutil" +) + +type TLSCertInfoCommand struct { + Meta +} + +func (c *TLSCertInfoCommand) Help() string { + helpText := ` +Usage: nomad tls cert info + + Show information about a TLS certificate. +` + return strings.TrimSpace(helpText) +} + +func (c *TLSCertInfoCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{}) +} + +func (c *TLSCertInfoCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictOr( + complete.PredictFiles("*.pem"), + ) +} + +func (c *TLSCertInfoCommand) Synopsis() string { + return "Show certificate information" +} + +func (c *TLSCertInfoCommand) Name() string { return "tls cert info" } + +func (c *TLSCertInfoCommand) Run(args []string) int { + + flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) + flagSet.Usage = func() { c.Ui.Output(c.Help()) } + + if err := flagSet.Parse(args); err != nil { + return 1 + } + + // Check that we got no arguments + args = flagSet.Args() + if l := len(args); l < 0 || l > 1 { + c.Ui.Error("This command takes up to one argument") + c.Ui.Error(commandErrorText(c)) + return 1 + } + var certFile []byte + var err error + var file string + + if len(args) == 1 { + file = args[0] + certFile, err = os.ReadFile(file) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading certificate file: %v", err)) + return 1 + } + } + + certInfo, err := tlsutil.ParseCert(string(certFile)) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + // Format the certificate info + basic := []string{ + fmt.Sprintf("Serial Number|%s", certInfo.SerialNumber), + fmt.Sprintf("Issuer CN|%s", certInfo.Issuer.CommonName), + fmt.Sprintf("Common Name|%s", certInfo.Subject), + fmt.Sprintf("Expiry Date|%s", certInfo.NotAfter), + fmt.Sprintf("DNS Names|%s", certInfo.DNSNames), + fmt.Sprintf("IP Addresses|%s", certInfo.IPAddresses), + } + + // Print out the information + c.Ui.Output(columnize.SimpleFormat(basic)) + return 0 +} diff --git a/helper/tlsutil/config.go b/helper/tlsutil/config.go index 8f5dbcbbe..bac61a4b9 100644 --- a/helper/tlsutil/config.go +++ b/helper/tlsutil/config.go @@ -6,8 +6,8 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" "net" + "os" "strings" "time" @@ -188,7 +188,7 @@ func (c *Config) AppendCA(pool *x509.CertPool) error { } // Read the file - data, err := ioutil.ReadFile(c.CAFile) + data, err := os.ReadFile(c.CAFile) if err != nil { return fmt.Errorf("Failed to read CA file: %v", err) } diff --git a/helper/tlsutil/config_test.go b/helper/tlsutil/config_test.go index 4a5afdb28..cd0d69d83 100644 --- a/helper/tlsutil/config_test.go +++ b/helper/tlsutil/config_test.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "fmt" "io" - "io/ioutil" "net" "os" "strings" @@ -89,7 +88,7 @@ TttDu+g2VdbcBwVDZ49X2Md6OY2N3G8Irdlj+n+mCQJaHwVt52DRzz0= -----END CERTIFICATE----- ` - tmpCAFile, err := ioutil.TempFile("/tmp", "test_ca_file") + tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file") require.NoError(err) defer os.Remove(tmpCAFile.Name()) @@ -167,7 +166,7 @@ TttDu+g2VdbcBwVDZ49X2Md6OY2N3G8Irdlj+n+mCQJaHwVt52DRzz0= ...outside of -----XXX----- blocks? ` - tmpCAFile, err := ioutil.TempFile("/tmp", "test_ca_file_extra") + tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file_extra") require.NoError(err) defer os.Remove(tmpCAFile.Name()) _, err = tmpCAFile.Write([]byte(certs)) @@ -210,7 +209,7 @@ Ln2ZUe8CIDsQswBQS7URbqnKYDye2Y4befJkr4fmhhmMQb2ex9A4 Invalid -----END CERTIFICATE-----` - tmpCAFile, err := ioutil.TempFile("/tmp", "test_ca_file") + tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file") require.NoError(err) defer os.Remove(tmpCAFile.Name()) _, err = tmpCAFile.Write([]byte(certs)) @@ -242,7 +241,7 @@ func TestConfig_AppendCA_Invalid(t *testing.T) { } { - tmpFile, err := ioutil.TempFile("/tmp", "test_ca_file") + tmpFile, err := os.CreateTemp("/tmp", "test_ca_file") require.Nil(err) defer os.Remove(tmpFile.Name()) _, err = tmpFile.Write([]byte("Invalid CA Content!")) @@ -493,7 +492,7 @@ func startTLSServer(config *Config) (net.Conn, chan error) { // server read any data from the client until error or // EOF, which will allow the client to Close(), and // *then* we Close() the server. - io.Copy(ioutil.Discard, tlsServer) + io.Copy(io.Discard, tlsServer) tlsServer.Close() }() return clientConn, errc diff --git a/helper/tlsutil/generate.go b/helper/tlsutil/generate.go index ecbba85cb..cf584cf1e 100644 --- a/helper/tlsutil/generate.go +++ b/helper/tlsutil/generate.go @@ -105,7 +105,7 @@ func GenerateCA(opts CAOpts) (string, string, error) { } name := opts.Name if name == "" { - name = fmt.Sprintf("Consul Agent CA %d", sn) + name = fmt.Sprintf("Nomad Agent CA %d", sn) } days := opts.Days @@ -229,6 +229,21 @@ func keyID(raw interface{}) ([]byte, error) { return kID[:], nil } +// ParseCert parses the x509 certificate from a PEM-encoded value. +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) +} + func parseCert(pemValue string) (*x509.Certificate, error) { // The _ result below is not an error but the remaining PEM bytes. block, _ := pem.Decode([]byte(pemValue)) diff --git a/helper/tlsutil/generate_test.go b/helper/tlsutil/generate_test.go index 8df3fb270..436da4e30 100644 --- a/helper/tlsutil/generate_test.go +++ b/helper/tlsutil/generate_test.go @@ -86,7 +86,7 @@ func TestGenerateCA(t *testing.T) { cert, err := parseCert(ca) require.Nil(t, err) - require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA")) + require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Nomad Agent CA")) require.Equal(t, true, cert.IsCA) require.Equal(t, true, cert.BasicConstraintsValid) @@ -104,7 +104,7 @@ func TestGenerateCA(t *testing.T) { cert, err := parseCert(ca) require.NoError(t, err) - require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA")) + require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Nomad Agent CA")) require.Equal(t, true, cert.IsCA) require.Equal(t, true, cert.BasicConstraintsValid) @@ -147,7 +147,7 @@ func TestGenerateCert(t *testing.T) { 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.Contains(t, cert.Issuer.CommonName, "Nomad Agent CA") require.Equal(t, false, cert.IsCA) require.WithinDuration(t, cert.NotBefore, time.Now(), time.Minute) diff --git a/lib/file/atomic.go b/lib/file/atomic.go new file mode 100644 index 000000000..fbc76e779 --- /dev/null +++ b/lib/file/atomic.go @@ -0,0 +1,51 @@ +package file + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/go-uuid" +) + +// WriteAtomicWithPerms creates a temp file with specific permissions and then renames and +// moves it to the path. +func WriteAtomicWithPerms(path string, contents []byte, dirPerms, filePerms os.FileMode) error { + + uuid, err := uuid.GenerateUUID() + if err != nil { + return err + } + tempPath := fmt.Sprintf("%s-%s.tmp", path, uuid) + + // Make a directory within the current one. + if err := os.MkdirAll(filepath.Dir(path), dirPerms); err != nil { + return err + } + + // File opened with write only permissions. Will be created if it does not exist + // file is given specific permissions + fh, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePerms) + if err != nil { + return err + } + + defer os.RemoveAll(tempPath) // clean up + + if _, err := fh.Write(contents); err != nil { + fh.Close() + return err + } + // Commits the current state of the file to disk + if err := fh.Sync(); err != nil { + fh.Close() + return err + } + if err := fh.Close(); err != nil { + return err + } + if err := os.Rename(tempPath, path); err != nil { + return err + } + return nil +} diff --git a/testutil/tls.go b/testutil/tls.go new file mode 100644 index 000000000..b5fa37acc --- /dev/null +++ b/testutil/tls.go @@ -0,0 +1,49 @@ +package testutil + +import ( + "crypto/x509" + "io/fs" + "os" + "testing" + + "github.com/hashicorp/nomad/helper/tlsutil" + "github.com/stretchr/testify/require" +) + +// Assert CA file exists and is a valid CA Returns the CA +func IsValidCertificate(t *testing.T, caPath string) *x509.Certificate { + t.Helper() + + require.FileExists(t, caPath) + caData, err := os.ReadFile(caPath) + require.NoError(t, err) + + ca, err := tlsutil.ParseCert(string(caData)) + require.NoError(t, err) + require.NotNil(t, ca) + + return ca +} + +// Assert key file exists and is a valid signer returns a bool +func IsValidSigner(t *testing.T, keyPath string) bool { + t.Helper() + + require.FileExists(t, keyPath) + fi, err := os.Stat(keyPath) + if err != nil { + t.Fatal("should not happen", err) + } + if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have { + t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have) + } + + keyData, err := os.ReadFile(keyPath) + require.NoError(t, err) + + signer, err := tlsutil.ParseSigner(string(keyData)) + require.NoError(t, err) + require.NotNil(t, signer) + + return true +} diff --git a/website/content/docs/commands/tls/ca-create.mdx b/website/content/docs/commands/tls/ca-create.mdx new file mode 100644 index 000000000..01deb51ec --- /dev/null +++ b/website/content/docs/commands/tls/ca-create.mdx @@ -0,0 +1,59 @@ +--- +layout: docs +page_title: 'Commands: tls ca create' +description: | + This command creates a Certificate Authority that can be used to create + self signed certificates to be used for Nomad TLS setup. +--- + +# Command: nomad tls ca create + +Create is used to create a self signed Certificate Authority to be used for +Nomad TLS setup. + +## Usage + +```plaintext +nomad tls ca create [options] +``` + +## CA Create Options + +- `-additional-domain=`: Add name constraints for the CA. The server will + reject certificates for DNS names other than those specified in `-domain` and + `-additional-domain`. Can be used multiple times. This option can only used in + combination with `-domain` and `-name-constraint`. + +- `common-name`: Common Name of CA. Defaults to Nomad Agent CA. + +- `-days=`: Provide number of days the CA is valid for from now on, + defaults to 5 years. + +- `-domain=`: Domain of nomad cluster. Only used in combination with + `-name-constraint`. Defaults to `nomad`. + +- `-name-constraint`: Add name constraints for the CA. Results in rejecting + certificates for other DNS than specified. If set to true, "localhost" and + `-domain` will be added to the allowed DNS. Defaults to false. + +~> **Warning:** If `-name-constraint` is enabled and you intend to serve the + Nomad web UI over HTTPS its DNS must be added with `additional-domain`. It is + not possible to add that after the fact. + +## Example + +Create CA: + +```shell-session +$ nomad tls ca create +==> CA Certificate saved to: nomad-agent-ca.pem +==> CA Certificate key saved to: nomad-agent-ca-key.pem +``` + +Create a CA with a specified domain: + +```shell-session +$ nomad tls ca create -name-constraint="true" -domain="foo.com" +==> CA Certificate saved to: foo.com-agent-ca.pem +==> CA Certificate key saved to: foo.com-agent-ca-key.pem +``` diff --git a/website/content/docs/commands/tls/ca-info.mdx b/website/content/docs/commands/tls/ca-info.mdx new file mode 100644 index 000000000..0261571b1 --- /dev/null +++ b/website/content/docs/commands/tls/ca-info.mdx @@ -0,0 +1,42 @@ +--- +layout: docs +page_title: 'Commands: tls ca info' +description: | + This command displays relevant information that is contained within a + Certificate Authority certificate. +--- + +# Command: nomad tls ca info + +Info is used to display relevant information that is contained within the +provided certificate file. + +## Usage + +```plaintext +nomad tls ca info +``` + +## Example + +Display default CA: + +```shell-session +$ nomad tls ca info nomad-agent-ca.pem +Serial Number 314623649437549144006237783956683542664 +Issuer CN Nomad Agent CA 314623649437549144006237783956683542664 +Common Name CN=Nomad Agent CA 314623649437549144006237783956683542664,O=HashiCorp Inc.,POSTALCODE=94105,STREET=101 Second Street,L=San Francisco,ST=CA,C=US +Expiry Date 2027-11-13 21:37:38 +0000 UTC +Permitted DNS Domains [] +``` + +Display CA with a custom domain "foo.com": + +```shell-session +$ nomad tls ca info foo.com-agent-ca.pem +Serial Number 189027561135335847320487296530900061259 +Issuer CN Nomad Agent CA 189027561135335847320487296530900061259 +Common Name CN=Nomad Agent CA 189027561135335847320487296530900061259,O=HashiCorp Inc.,POSTALCODE=94105,STREET=101 Second Street,L=San Francisco,ST=CA,C=US +Expiry Date 2027-11-13 21:38:15 +0000 UTC +Permitted DNS Domains [foo.com localhost] +``` diff --git a/website/content/docs/commands/tls/cert-create.mdx b/website/content/docs/commands/tls/cert-create.mdx new file mode 100644 index 000000000..8b2f46cd7 --- /dev/null +++ b/website/content/docs/commands/tls/cert-create.mdx @@ -0,0 +1,86 @@ +--- +layout: docs +page_title: 'Commands: TLS Cert Create' +description: | + This command creates a Certificate that can be used for Nomad TLS setup. +--- + +# Command: nomad tls cert create + +The `tls cert create` command is used to create certificates to be used for +[TLS encryption][] for your Nomad cluster. You can then copy these to your +servers and clients. This command will not automatically update the +configuration of the agents. + +## Usage + +Usage: `nomad tls cert create [options]` + +#### Command Options + +- `-additional-dnsname=`: Provide an additional dnsname for Subject + Alternative Names. `localhost` is always included. This flag may be provided + multiple times. + +- `-additional-ipaddress=`: Provide an additional ipaddress for Subject + Alternative Names. `127.0.0.1` is always included. This flag may be provided + multiple times. + +- `-ca=`: Provide path to the ca. Defaults to `#DOMAIN#-agent-ca.pem`. + +- `-cli`: Generate cli certificate. + +- `-client`: Generate client certificate. + +- `-days=`: Provide number of days the certificate is valid for from now + on. Defaults to 1 year. + +- `-dc=`: Provide the datacenter. Matters only for `-server` + certificates. Defaults to `dc1`. + +- `-domain=`: Provide the domain. Matters only for `-server` + certificates. + +- `-key=`: Provide path to the key. Defaults to + `#DOMAIN#-agent-ca-key.pem`. + +- `-node=`: When generating a server cert and this server is set an + additional DNS name is included of the form + `.server..`. + +- `-server`: Generate server certificate. + +## Examples + +Create a certificate for servers: + +```shell-session +$ nomad 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 CA file nomad-agent-ca.pem and CA key nomad-agent-ca-key.pem +==> Server Certificate saved to global-server-nomad.pem +==> Server Certificate key saved to global-server-nomad-key.pem +``` + +Create a certificate for clients: + +```shell-session +$ nomad tls cert create -client +==> Using CA file nomad-agent-ca.pem and CA key nomad-agent-ca-key.pem +==> Client Certificate saved to global-client-nomad.pem +==> Client Certificate key saved to global-client-nomad-key.pem +``` + +Create a certificate for the CLI: + +```shell-session +$ nomad tls cert create -cli +==> Using CA file nomad-agent-ca.pem and CA key nomad-agent-ca-key.pem +==> Cli Certificate saved to global-cli-nomad.pem +==> Cli Certificate key saved to global-cli-nomad-key.pem +``` + +[TLS encryption]: https://learn.hashicorp.com/tutorials/nomad/security-enable-tls?in=nomad/transport-security diff --git a/website/content/docs/commands/tls/cert-info.mdx b/website/content/docs/commands/tls/cert-info.mdx new file mode 100644 index 000000000..c37f6a54c --- /dev/null +++ b/website/content/docs/commands/tls/cert-info.mdx @@ -0,0 +1,32 @@ +--- +layout: docs +page_title: 'Commands: TLS Cert Info' +description: | + This command displays relevant information that is contained within a + certificate. +--- + +# Command: nomad tls cert info + +Info is used to display relevant information that is contained within a provided +certificate file. + +## Usage + +```plaintext +nomad tls cert info +``` + +## Examples + +Display default certificate info: + +```shell-session +$ nomad tls cert info global-cli-nomad.pem +Serial Number 307777061759235334129808343588809897525 +Issuer CN Nomad Agent CA 314623649437549144006237783956683542664 +Common Name CN=cli.global.nomad +Expiry Date 2023-11-14 21:40:45 +0000 UTC +DNS Names [cli.global.nomad localhost] +IP Addresses [] +``` diff --git a/website/content/docs/commands/tls/index.mdx b/website/content/docs/commands/tls/index.mdx new file mode 100644 index 000000000..61647a5ae --- /dev/null +++ b/website/content/docs/commands/tls/index.mdx @@ -0,0 +1,28 @@ +--- +layout: docs +page_title: 'Commands: tls' +description: | + The tls command is used to help with creating a Certificate Authority + and up self signed certificates for Nomad TLS configuration. +--- + +# Command: tls + +The `tls` command is used to help with setting up a self signed CA and certificates for Nomad TLS. + +## Usage + +Usage: `nomad tls [options]` + +Run `nomad tls -h` for help on that subcommand. The following +subcommands are available: + +- [`ca create`][cacreate] - Create Certificate Authority +- [`ca info`][cainfo] - Display information from a CA certificate +- [`cert create`][certcreate] - Create self signed certificates +- [`cert info`][certinfo] - Display information from a certificate + +[cacreate]: /docs/commands/tls/ca-create 'Create Certificate Authority' +[cainfo]: /docs/commands/tls/ca-info 'Display information from a CA certificate' +[certcreate]: /docs/commands/tls/cert-create 'Create self signed certificates' +[certinfo]: /docs/commands/tls/cert-info 'Display information from a certificate' \ No newline at end of file diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 398f1abc1..a3c746a31 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -904,6 +904,31 @@ } ] }, + { + "title": "tls", + "routes": [ + { + "title": "Overview", + "path": "commands/tls" + }, + { + "title": "ca create", + "path": "commands/tls/ca-create" + }, + { + "title": "ca info", + "path": "commands/tls/ca-info" + }, + { + "title": "cert create", + "path": "commands/tls/cert-create" + }, + { + "title": "cert info", + "path": "commands/tls/cert-info" + } + ] + }, { "title": "ui", "path": "commands/ui"