diff --git a/changelog/16421.txt b/changelog/16421.txt new file mode 100644 index 0000000000000..281d2e8717346 --- /dev/null +++ b/changelog/16421.txt @@ -0,0 +1,3 @@ +```release-note:improvement +command/server: add `-dev-tls` and `-dev-tls-cert-dir` subcommands to create a Vault dev server with generated certificates and private key. +``` diff --git a/command/server.go b/command/server.go index 52b84e6160b96..32b48325b2a47 100644 --- a/command/server.go +++ b/command/server.go @@ -117,6 +117,8 @@ type ServerCommand struct { flagLogFormat string flagRecovery bool flagDev bool + flagDevTLS bool + flagDevTLSCertDir string flagDevRootTokenID string flagDevListenAddr string flagDevNoStoreToken bool @@ -245,6 +247,23 @@ func (c *ServerCommand) Flags() *FlagSets { "production.", }) + f.BoolVar(&BoolVar{ + Name: "dev-tls", + Target: &c.flagDevTLS, + Usage: "Enable TLS development mode. In this mode, Vault runs in-memory and " + + "starts unsealed, with a generated TLS CA, certificate and key. " + + "As the name implies, do not run \"dev-tls\" mode in " + + "production.", + }) + + f.StringVar(&StringVar{ + Name: "dev-tls-cert-dir", + Target: &c.flagDevTLSCertDir, + Default: "", + Usage: "Directory where generated TLS files are created if `-dev-tls` is " + + "specified. If left unset, files are generated in a temporary directory.", + }) + f.StringVar(&StringVar{ Name: "dev-root-token-id", Target: &c.flagDevRootTokenID, @@ -1026,7 +1045,7 @@ func (c *ServerCommand) Run(args []string) int { } // Automatically enable dev mode if other dev flags are provided. - if c.flagDevConsul || c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 { + if c.flagDevConsul || c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS { c.flagDev = true } @@ -1062,6 +1081,7 @@ func (c *ServerCommand) Run(args []string) int { // Load the configuration var config *server.Config var err error + var certDir string if c.flagDev { var devStorageType string switch { @@ -1076,11 +1096,59 @@ func (c *ServerCommand) Run(args []string) int { default: devStorageType = "inmem" } - config, err = server.DevConfig(devStorageType) + + if c.flagDevTLS { + if c.flagDevTLSCertDir != "" { + _, err := os.Stat(c.flagDevTLSCertDir) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + certDir = c.flagDevTLSCertDir + } else { + certDir, err = os.MkdirTemp("", "vault-tls") + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + } + config, err = server.DevTLSConfig(devStorageType, certDir) + + defer func() { + err := os.Remove(fmt.Sprintf("%s/%s", certDir, server.VaultDevCAFilename)) + if err != nil { + c.UI.Error(err.Error()) + } + + err = os.Remove(fmt.Sprintf("%s/%s", certDir, server.VaultDevCertFilename)) + if err != nil { + c.UI.Error(err.Error()) + } + + err = os.Remove(fmt.Sprintf("%s/%s", certDir, server.VaultDevKeyFilename)) + if err != nil { + c.UI.Error(err.Error()) + } + + // Only delete temp directories we made. + if c.flagDevTLSCertDir == "" { + err = os.Remove(certDir) + if err != nil { + c.UI.Error(err.Error()) + } + } + }() + + } else { + config, err = server.DevConfig(devStorageType) + } + if err != nil { c.UI.Error(err.Error()) return 1 } + if c.flagDevListenAddr != "" { config.Listeners[0].Address = c.flagDevListenAddr } @@ -1495,7 +1563,7 @@ func (c *ServerCommand) Run(args []string) int { } // If we're in Dev mode, then initialize the core - err = initDevCore(c, &coreConfig, config, core) + err = initDevCore(c, &coreConfig, config, core, certDir) if err != nil { c.UI.Error(err.Error()) return 1 @@ -2442,7 +2510,11 @@ func determineRedirectAddr(c *ServerCommand, coreConfig *vault.CoreConfig, confi } } if coreConfig.RedirectAddr == "" && c.flagDev { - coreConfig.RedirectAddr = fmt.Sprintf("http://%s", config.Listeners[0].Address) + protocol := "http" + if c.flagDevTLS { + protocol = "https" + } + coreConfig.RedirectAddr = fmt.Sprintf("%s://%s", protocol, config.Listeners[0].Address) } return retErr } @@ -2604,7 +2676,7 @@ func runListeners(c *ServerCommand, coreConfig *vault.CoreConfig, config *server return nil } -func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server.Config, core *vault.Core) error { +func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server.Config, core *vault.Core, certDir string) error { if c.flagDev && !c.flagDevSkipInit { init, err := c.enableDev(core, coreConfig) @@ -2655,10 +2727,15 @@ func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server. "token is already authenticated to the CLI, so you can immediately " + "begin using Vault.")) c.UI.Warn("") - c.UI.Warn("You may need to set the following environment variable:") + c.UI.Warn("You may need to set the following environment variables:") c.UI.Warn("") - endpointURL := "http://" + config.Listeners[0].Address + protocol := "http://" + if c.flagDevTLS { + protocol = "https://" + } + + endpointURL := protocol + config.Listeners[0].Address if runtime.GOOS == "windows" { c.UI.Warn("PowerShell:") c.UI.Warn(fmt.Sprintf(" $env:VAULT_ADDR=\"%s\"", endpointURL)) @@ -2668,6 +2745,18 @@ func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server. c.UI.Warn(fmt.Sprintf(" $ export VAULT_ADDR='%s'", endpointURL)) } + if c.flagDevTLS { + if runtime.GOOS == "windows" { + c.UI.Warn("PowerShell:") + c.UI.Warn(fmt.Sprintf(" $env:VAULT_CACERT=\"%s/vault-ca.pem\"", certDir)) + c.UI.Warn("cmd.exe:") + c.UI.Warn(fmt.Sprintf(" set VAULT_CACERT=%s/vault-ca.pem", certDir)) + } else { + c.UI.Warn(fmt.Sprintf(" $ export VAULT_CACERT='%s/vault-ca.pem'", certDir)) + } + c.UI.Warn("") + } + // Unseal key is not returned if stored shares is supported if len(init.SecretShares) > 0 { c.UI.Warn("") diff --git a/command/server/config.go b/command/server/config.go index 1199989c691f4..5d985db69d08d 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -22,6 +22,12 @@ import ( "github.com/hashicorp/vault/sdk/helper/consts" ) +const ( + VaultDevCAFilename = "vault-ca.pem" + VaultDevCertFilename = "vault-cert.pem" + VaultDevKeyFilename = "vault-key.pem" +) + var entConfigValidate = func(_ *Config, _ string) []configutil.ConfigError { return nil } @@ -151,6 +157,62 @@ ui = true return parsed, nil } +// DevTLSConfig is a Config that is used for dev tls mode of Vault. +func DevTLSConfig(storageType, certDir string) (*Config, error) { + ca, err := GenerateCA() + if err != nil { + return nil, err + } + + cert, key, err := GenerateCert(ca.Template, ca.Signer) + if err != nil { + return nil, err + } + + if err := os.WriteFile(fmt.Sprintf("%s/%s", certDir, VaultDevCAFilename), []byte(ca.PEM), 0o444); err != nil { + return nil, err + } + + if err := os.WriteFile(fmt.Sprintf("%s/%s", certDir, VaultDevCertFilename), []byte(cert), 0o400); err != nil { + return nil, err + } + + if err := os.WriteFile(fmt.Sprintf("%s/%s", certDir, VaultDevKeyFilename), []byte(key), 0o400); err != nil { + return nil, err + } + + hclStr := ` +disable_mlock = true + +listener "tcp" { + address = "[::]:8200" + tls_cert_file = "%s/vault-cert.pem" + tls_key_file = "%s/vault-key.pem" + proxy_protocol_behavior = "allow_authorized" + proxy_protocol_authorized_addrs = "[::]:8200" +} + +telemetry { + prometheus_retention_time = "24h" + disable_hostname = true +} +enable_raw_endpoint = true + +storage "%s" { +} + +ui = true +` + + hclStr = fmt.Sprintf(hclStr, certDir, certDir, storageType) + parsed, err := ParseConfig(hclStr, "") + if err != nil { + return nil, err + } + + return parsed, nil +} + // Storage is the underlying storage configuration for the server. type Storage struct { Type string diff --git a/command/server/tls_util.go b/command/server/tls_util.go new file mode 100644 index 0000000000000..d327006332511 --- /dev/null +++ b/command/server/tls_util.go @@ -0,0 +1,162 @@ +package server + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "time" + + "github.com/hashicorp/vault/sdk/helper/certutil" +) + +type CaCert struct { + PEM string + Template *x509.Certificate + Signer crypto.Signer +} + +// GenerateCert creates a new leaf cert from provided CA template and signer +func GenerateCert(caCertTemplate *x509.Certificate, caSigner crypto.Signer) (string, string, error) { + // Create the private key + signer, keyPEM, err := privateKey() + if err != nil { + return "", "", fmt.Errorf("error generating private key for server certificate: %v", err) + } + + // The serial number for the cert + sn, err := serialNumber() + if err != nil { + return "", "", fmt.Errorf("error generating serial number: %v", err) + } + + signerKeyId, err := certutil.GetSubjKeyID(signer) + if err != nil { + return "", "", fmt.Errorf("error getting subject key id from key: %v", err) + } + + hostname, err := os.Hostname() + if err != nil { + return "", "", fmt.Errorf("error getting hostname: %v", err) + } + + if hostname == "" { + hostname = "localhost" + } + + // Create the leaf cert + template := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{CommonName: hostname}, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + NotBefore: time.Now().Add(-1 * time.Minute), + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: []string{"localhost", "localhost4", "localhost6", "localhost.localdomain"}, + AuthorityKeyId: caCertTemplate.AuthorityKeyId, + SubjectKeyId: signerKeyId, + } + + bs, err := x509.CreateCertificate( + rand.Reader, &template, caCertTemplate, signer.Public(), caSigner) + if err != nil { + return "", "", fmt.Errorf("error creating server certificate: %v", err) + } + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) + if err != nil { + return "", "", fmt.Errorf("error encoding server certificate: %v", err) + } + + return buf.String(), keyPEM, nil +} + +// GenerateCA generates a new self-signed CA cert and returns a +// CaCert struct containing the PEM encoded cert, +// X509 Certificate Template, and crypto.Signer +func GenerateCA() (*CaCert, error) { + // Create the private key we'll use for this CA cert. + signer, _, err := privateKey() + if err != nil { + return nil, fmt.Errorf("error generating private key for CA: %v", err) + } + + signerKeyId, err := certutil.GetSubjKeyID(signer) + if err != nil { + return nil, fmt.Errorf("error getting subject key id from key: %v", err) + } + + // The serial number for the cert + sn, err := serialNumber() + if err != nil { + return nil, fmt.Errorf("error generating serial number: %v", err) + } + + // Create the CA cert + template := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{CommonName: "Vault Dev CA"}, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + IsCA: true, + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + NotBefore: time.Now().Add(-1 * time.Minute), + AuthorityKeyId: signerKeyId, + SubjectKeyId: signerKeyId, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + bs, err := x509.CreateCertificate( + rand.Reader, &template, &template, signer.Public(), signer) + if err != nil { + return nil, fmt.Errorf("error creating CA certificate: %v", err) + } + + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) + if err != nil { + return nil, fmt.Errorf("error encoding CA certificate: %v", err) + } + return &CaCert{ + PEM: buf.String(), + Template: &template, + Signer: signer, + }, nil +} + +// privateKey returns a new ECDSA-based private key. Both a crypto.Signer +// and the key in PEM format are returned. +func privateKey() (crypto.Signer, string, error) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, "", err + } + + bs, err := x509.MarshalECPrivateKey(pk) + if err != nil { + return nil, "", err + } + + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) + if err != nil { + return nil, "", err + } + + return pk, buf.String(), nil +} + +// serialNumber generates a new random serial number. +func serialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) +} diff --git a/website/content/docs/commands/server.mdx b/website/content/docs/commands/server.mdx index 25bda80c5cba7..929aa5281e369 100644 --- a/website/content/docs/commands/server.mdx +++ b/website/content/docs/commands/server.mdx @@ -66,6 +66,12 @@ flags](/docs/commands) included on all commands. in-memory and starts unsealed. As the name implies, do not run "dev" mode in production. +- `-dev-tls` `(bool: false)` - Enable TLS development mode. In this mode, Vault runs + in-memory and starts unsealed with a generated TLS CA, certificate and key. + As the name implies, do not run "dev" mode in production. + +- `-dev-tls-cert-dir` `(string: "")` - Directory where generated TLS files are created if `-dev-tls` is specified. If left unset, files are generated in a temporary directory. + - `-dev-listen-address` `(string: "127.0.0.1:8200")` - Address to bind to in "dev" mode. This can also be specified via the `VAULT_DEV_LISTEN_ADDRESS` environment variable.