command/server: add dev-tls flag (#16421)

* command/server: add dev-tls flag

* Add website documentation

* changelog

* Lower file permissions

* Update cert gen per review

* Add dev-tls-cert-dir flag and cert clean up

* fmt

* Update cert generation per review

* Remove unused function

* Add better error messages

* Log errors in cleanup, fix directory not existing bug

* Remove hidden flag from -dev-tls-cert-dir

* Add usage

* Update 16421.txt

* Update variable names for files

* Remove directory on cleanup
This commit is contained in:
Jason O'Donnell 2022-07-22 14:04:03 -04:00 committed by GitHub
parent 45be51df49
commit 140406143e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 329 additions and 7 deletions

3
changelog/16421.txt Normal file
View File

@ -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.
```

View File

@ -117,6 +117,8 @@ type ServerCommand struct {
flagLogFormat string flagLogFormat string
flagRecovery bool flagRecovery bool
flagDev bool flagDev bool
flagDevTLS bool
flagDevTLSCertDir string
flagDevRootTokenID string flagDevRootTokenID string
flagDevListenAddr string flagDevListenAddr string
flagDevNoStoreToken bool flagDevNoStoreToken bool
@ -245,6 +247,23 @@ func (c *ServerCommand) Flags() *FlagSets {
"production.", "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{ f.StringVar(&StringVar{
Name: "dev-root-token-id", Name: "dev-root-token-id",
Target: &c.flagDevRootTokenID, Target: &c.flagDevRootTokenID,
@ -1026,7 +1045,7 @@ func (c *ServerCommand) Run(args []string) int {
} }
// Automatically enable dev mode if other dev flags are provided. // 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 c.flagDev = true
} }
@ -1062,6 +1081,7 @@ func (c *ServerCommand) Run(args []string) int {
// Load the configuration // Load the configuration
var config *server.Config var config *server.Config
var err error var err error
var certDir string
if c.flagDev { if c.flagDev {
var devStorageType string var devStorageType string
switch { switch {
@ -1076,11 +1096,59 @@ func (c *ServerCommand) Run(args []string) int {
default: default:
devStorageType = "inmem" 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 { if err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 1 return 1
} }
if c.flagDevListenAddr != "" { if c.flagDevListenAddr != "" {
config.Listeners[0].Address = 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 // 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 { if err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 1 return 1
@ -2442,7 +2510,11 @@ func determineRedirectAddr(c *ServerCommand, coreConfig *vault.CoreConfig, confi
} }
} }
if coreConfig.RedirectAddr == "" && c.flagDev { 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 return retErr
} }
@ -2604,7 +2676,7 @@ func runListeners(c *ServerCommand, coreConfig *vault.CoreConfig, config *server
return nil 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 { if c.flagDev && !c.flagDevSkipInit {
init, err := c.enableDev(core, coreConfig) 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 " + "token is already authenticated to the CLI, so you can immediately " +
"begin using Vault.")) "begin using Vault."))
c.UI.Warn("") 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("") 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" { if runtime.GOOS == "windows" {
c.UI.Warn("PowerShell:") c.UI.Warn("PowerShell:")
c.UI.Warn(fmt.Sprintf(" $env:VAULT_ADDR=\"%s\"", endpointURL)) 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)) 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 // Unseal key is not returned if stored shares is supported
if len(init.SecretShares) > 0 { if len(init.SecretShares) > 0 {
c.UI.Warn("") c.UI.Warn("")

View File

@ -22,6 +22,12 @@ import (
"github.com/hashicorp/vault/sdk/helper/consts" "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 { var entConfigValidate = func(_ *Config, _ string) []configutil.ConfigError {
return nil return nil
} }
@ -151,6 +157,62 @@ ui = true
return parsed, nil 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. // Storage is the underlying storage configuration for the server.
type Storage struct { type Storage struct {
Type string Type string

162
command/server/tls_util.go Normal file
View File

@ -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))
}

View File

@ -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 in-memory and starts unsealed. As the name implies, do not run "dev" mode in
production. 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-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` "dev" mode. This can also be specified via the `VAULT_DEV_LISTEN_ADDRESS`
environment variable. environment variable.