mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-07 23:27:01 +02:00
* OpenAPI: Separate ListOperation from ReadOperation Historically, since Vault's ReadOperation and ListOperation both map to the HTTP GET method, their representation in the generated OpenAPI has been a bit confusing. This was partially mitigated some time ago, by making the `list` query parameter express whether it was required or optional - but only in a way useful to human readers - the human had to know, for example, that the schema of the response body would change depending on whether `list` was selected. Now that there is an effort underway to automatically generate API clients from the OpenAPI spec, we have a need to fix this more comprehensively. Fortunately, we do have a means to do so - since Vault has opinionated treatment of trailing slashes, linked to operations being list or not, we can use an added trailing slash on the URL path to separate list operations in the OpenAPI spec. This PR implements that, and then fixes an operation ID which becomes duplicated, with this change applied. See also hashicorp/vault-client-go#174, a bug which will be fixed by this work. * Set further DisplayAttrs in auth/github plugin To mask out more duplicate read/list functionality, now being separately generated to OpenAPI client libraries as a result of this change. * Apply requested changes to operation IDs I'm not totally convinced its worth the extra lines of code, but equally, I don't have strong feelings about it, so I'll just make the change. * Adjust logic to prevent any possibility of generating OpenAPI paths with doubled final slashes Even in the edge case of improper use of regex patterns and operations. * changelog * Fix TestSudoPaths to pass again... which snowballed a bit... Once I looked hard at it, I found it was missing several sudo paths, which led to additional bug fixing elsewhere. I might need to pull some parts of this change out into a separate PR for ease of review... * Fix other tests * More test fixing * Undo scope creep - back away from fixing sudo paths not shown as such in OpenAPI, at least within this PR Just add TODO comments for now.
282 lines
11 KiB
Go
282 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"flag"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
|
|
"github.com/go-jose/go-jose/v3/jwt"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
)
|
|
|
|
const (
|
|
// PluginAutoMTLSEnv is used to ensure AutoMTLS is used. This will override
|
|
// setting a TLSProviderFunc for a plugin.
|
|
PluginAutoMTLSEnv = "VAULT_PLUGIN_AUTOMTLS_ENABLED"
|
|
|
|
// PluginMetadataModeEnv is an ENV name used to disable TLS communication
|
|
// to bootstrap mounting plugins.
|
|
PluginMetadataModeEnv = "VAULT_PLUGIN_METADATA_MODE"
|
|
|
|
// PluginUnwrapTokenEnv is the ENV name used to pass unwrap tokens to the
|
|
// plugin.
|
|
PluginUnwrapTokenEnv = "VAULT_UNWRAP_TOKEN"
|
|
)
|
|
|
|
// sudoPaths is a map containing the paths that require a token's policy
|
|
// to have the "sudo" capability. The keys are the paths as strings, in
|
|
// the same format as they are returned by the OpenAPI spec. The values
|
|
// are the regular expressions that can be used to test whether a given
|
|
// path matches that path or not (useful specifically for the paths that
|
|
// contain templated fields.)
|
|
var sudoPaths = map[string]*regexp.Regexp{
|
|
"/auth/token/accessors": regexp.MustCompile(`^/auth/token/accessors/?$`),
|
|
// TODO /auth/token/revoke-orphan requires sudo but isn't represented as such in the OpenAPI spec
|
|
"/pki/root": regexp.MustCompile(`^/pki/root$`),
|
|
"/pki/root/sign-self-issued": regexp.MustCompile(`^/pki/root/sign-self-issued$`),
|
|
"/sys/audit": regexp.MustCompile(`^/sys/audit$`),
|
|
"/sys/audit/{path}": regexp.MustCompile(`^/sys/audit/.+$`),
|
|
"/sys/auth/{path}": regexp.MustCompile(`^/sys/auth/.+$`),
|
|
"/sys/auth/{path}/tune": regexp.MustCompile(`^/sys/auth/.+/tune$`),
|
|
"/sys/config/auditing/request-headers": regexp.MustCompile(`^/sys/config/auditing/request-headers$`),
|
|
"/sys/config/auditing/request-headers/{header}": regexp.MustCompile(`^/sys/config/auditing/request-headers/.+$`),
|
|
"/sys/config/cors": regexp.MustCompile(`^/sys/config/cors$`),
|
|
"/sys/config/ui/headers": regexp.MustCompile(`^/sys/config/ui/headers/?$`),
|
|
"/sys/config/ui/headers/{header}": regexp.MustCompile(`^/sys/config/ui/headers/.+$`),
|
|
"/sys/internal/inspect/router/{tag}": regexp.MustCompile(`^/sys/internal/inspect/router/.+$`),
|
|
"/sys/leases": regexp.MustCompile(`^/sys/leases$`),
|
|
// This entry is a bit wrong... sys/leases/lookup does NOT require sudo. But sys/leases/lookup/ with a trailing
|
|
// slash DOES require sudo. But the part of the Vault CLI that uses this logic doesn't pass operation-appropriate
|
|
// trailing slashes, it always strips them off, so we end up giving the wrong answer for one of these.
|
|
"/sys/leases/lookup": regexp.MustCompile(`^/sys/leases/lookup/?$`),
|
|
"/sys/leases/lookup/{prefix}": regexp.MustCompile(`^/sys/leases/lookup/.+$`),
|
|
"/sys/leases/revoke-force/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-force/.+$`),
|
|
"/sys/leases/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-prefix/.+$`),
|
|
"/sys/plugins/catalog/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[^/]+$`),
|
|
"/sys/plugins/catalog/{type}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+$`),
|
|
"/sys/plugins/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+/[^/]+$`),
|
|
"/sys/raw": regexp.MustCompile(`^/sys/raw$`),
|
|
"/sys/raw/{path}": regexp.MustCompile(`^/sys/raw/.+$`),
|
|
"/sys/remount": regexp.MustCompile(`^/sys/remount$`),
|
|
"/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`),
|
|
"/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`),
|
|
"/sys/rotate": regexp.MustCompile(`^/sys/rotate$`),
|
|
// TODO /sys/seal requires sudo but isn't represented as such in the OpenAPI spec
|
|
// TODO /sys/step-down requires sudo but isn't represented as such in the OpenAPI spec
|
|
|
|
// enterprise-only paths
|
|
"/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`),
|
|
"/sys/replication/performance/primary/secondary-token": regexp.MustCompile(`^/sys/replication/performance/primary/secondary-token$`),
|
|
"/sys/replication/primary/secondary-token": regexp.MustCompile(`^/sys/replication/primary/secondary-token$`),
|
|
"/sys/replication/reindex": regexp.MustCompile(`^/sys/replication/reindex$`),
|
|
"/sys/storage/raft/snapshot-auto/config": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/?$`),
|
|
"/sys/storage/raft/snapshot-auto/config/{name}": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/[^/]+$`),
|
|
}
|
|
|
|
// PluginAPIClientMeta is a helper that plugins can use to configure TLS connections
|
|
// back to Vault.
|
|
type PluginAPIClientMeta struct {
|
|
// These are set by the command line flags.
|
|
flagCACert string
|
|
flagCAPath string
|
|
flagClientCert string
|
|
flagClientKey string
|
|
flagInsecure bool
|
|
}
|
|
|
|
// FlagSet returns the flag set for configuring the TLS connection
|
|
func (f *PluginAPIClientMeta) FlagSet() *flag.FlagSet {
|
|
fs := flag.NewFlagSet("vault plugin settings", flag.ContinueOnError)
|
|
|
|
fs.StringVar(&f.flagCACert, "ca-cert", "", "")
|
|
fs.StringVar(&f.flagCAPath, "ca-path", "", "")
|
|
fs.StringVar(&f.flagClientCert, "client-cert", "", "")
|
|
fs.StringVar(&f.flagClientKey, "client-key", "", "")
|
|
fs.BoolVar(&f.flagInsecure, "tls-skip-verify", false, "")
|
|
|
|
return fs
|
|
}
|
|
|
|
// GetTLSConfig will return a TLSConfig based off the values from the flags
|
|
func (f *PluginAPIClientMeta) GetTLSConfig() *TLSConfig {
|
|
// If we need custom TLS configuration, then set it
|
|
if f.flagCACert != "" || f.flagCAPath != "" || f.flagClientCert != "" || f.flagClientKey != "" || f.flagInsecure {
|
|
t := &TLSConfig{
|
|
CACert: f.flagCACert,
|
|
CAPath: f.flagCAPath,
|
|
ClientCert: f.flagClientCert,
|
|
ClientKey: f.flagClientKey,
|
|
TLSServerName: "",
|
|
Insecure: f.flagInsecure,
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VaultPluginTLSProvider wraps VaultPluginTLSProviderContext using context.Background.
|
|
func VaultPluginTLSProvider(apiTLSConfig *TLSConfig) func() (*tls.Config, error) {
|
|
return VaultPluginTLSProviderContext(context.Background(), apiTLSConfig)
|
|
}
|
|
|
|
// VaultPluginTLSProviderContext is run inside a plugin and retrieves the response
|
|
// wrapped TLS certificate from vault. It returns a configured TLS Config.
|
|
func VaultPluginTLSProviderContext(ctx context.Context, apiTLSConfig *TLSConfig) func() (*tls.Config, error) {
|
|
if os.Getenv(PluginAutoMTLSEnv) == "true" || os.Getenv(PluginMetadataModeEnv) == "true" {
|
|
return nil
|
|
}
|
|
|
|
return func() (*tls.Config, error) {
|
|
unwrapToken := os.Getenv(PluginUnwrapTokenEnv)
|
|
|
|
parsedJWT, err := jwt.ParseSigned(unwrapToken)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing wrapping token: {{err}}", err)
|
|
}
|
|
|
|
allClaims := make(map[string]interface{})
|
|
if err = parsedJWT.UnsafeClaimsWithoutVerification(&allClaims); err != nil {
|
|
return nil, errwrap.Wrapf("error parsing claims from wrapping token: {{err}}", err)
|
|
}
|
|
|
|
addrClaimRaw, ok := allClaims["addr"]
|
|
if !ok {
|
|
return nil, errors.New("could not validate addr claim")
|
|
}
|
|
vaultAddr, ok := addrClaimRaw.(string)
|
|
if !ok {
|
|
return nil, errors.New("could not parse addr claim")
|
|
}
|
|
if vaultAddr == "" {
|
|
return nil, errors.New(`no vault api_addr found`)
|
|
}
|
|
|
|
// Sanity check the value
|
|
if _, err := url.Parse(vaultAddr); err != nil {
|
|
return nil, errwrap.Wrapf("error parsing the vault api_addr: {{err}}", err)
|
|
}
|
|
|
|
// Unwrap the token
|
|
clientConf := DefaultConfig()
|
|
clientConf.Address = vaultAddr
|
|
if apiTLSConfig != nil {
|
|
err := clientConf.ConfigureTLS(apiTLSConfig)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error configuring api client {{err}}", err)
|
|
}
|
|
}
|
|
client, err := NewClient(clientConf)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error during api client creation: {{err}}", err)
|
|
}
|
|
|
|
// Reset token value to make sure nothing has been set by default
|
|
client.ClearToken()
|
|
|
|
secret, err := client.Logical().UnwrapWithContext(ctx, unwrapToken)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error during token unwrap request: {{err}}", err)
|
|
}
|
|
if secret == nil {
|
|
return nil, errors.New("error during token unwrap request: secret is nil")
|
|
}
|
|
|
|
// Retrieve and parse the server's certificate
|
|
serverCertBytesRaw, ok := secret.Data["ServerCert"].(string)
|
|
if !ok {
|
|
return nil, errors.New("error unmarshalling certificate")
|
|
}
|
|
|
|
serverCertBytes, err := base64.StdEncoding.DecodeString(serverCertBytesRaw)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing certificate: {{err}}", err)
|
|
}
|
|
|
|
serverCert, err := x509.ParseCertificate(serverCertBytes)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing certificate: {{err}}", err)
|
|
}
|
|
|
|
// Retrieve and parse the server's private key
|
|
serverKeyB64, ok := secret.Data["ServerKey"].(string)
|
|
if !ok {
|
|
return nil, errors.New("error unmarshalling certificate")
|
|
}
|
|
|
|
serverKeyRaw, err := base64.StdEncoding.DecodeString(serverKeyB64)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing certificate: {{err}}", err)
|
|
}
|
|
|
|
serverKey, err := x509.ParseECPrivateKey(serverKeyRaw)
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing certificate: {{err}}", err)
|
|
}
|
|
|
|
// Add CA cert to the cert pool
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AddCert(serverCert)
|
|
|
|
// Build a certificate object out of the server's cert and private key.
|
|
cert := tls.Certificate{
|
|
Certificate: [][]byte{serverCertBytes},
|
|
PrivateKey: serverKey,
|
|
Leaf: serverCert,
|
|
}
|
|
|
|
// Setup TLS config
|
|
tlsConfig := &tls.Config{
|
|
ClientCAs: caCertPool,
|
|
RootCAs: caCertPool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
// TLS 1.2 minimum
|
|
MinVersion: tls.VersionTLS12,
|
|
Certificates: []tls.Certificate{cert},
|
|
ServerName: serverCert.Subject.CommonName,
|
|
}
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
}
|
|
|
|
func SudoPaths() map[string]*regexp.Regexp {
|
|
return sudoPaths
|
|
}
|
|
|
|
// Determine whether the given path requires the sudo capability.
|
|
// Note that this uses hardcoded static path information, so will return incorrect results for paths in namespaces,
|
|
// or for secret engines mounted at non-default paths.
|
|
// Expects to receive a path with an initial slash, but no trailing slashes, as the Vault CLI (the only known and
|
|
// expected user of this function) sanitizes its paths that way.
|
|
func IsSudoPath(path string) bool {
|
|
// Return early if the path is any of the non-templated sudo paths.
|
|
if _, ok := sudoPaths[path]; ok {
|
|
return true
|
|
}
|
|
|
|
// Some sudo paths have templated fields in them.
|
|
// (e.g. /sys/revoke-prefix/{prefix})
|
|
// The values in the sudoPaths map are actually regular expressions,
|
|
// so we can check if our path matches against them.
|
|
for _, sudoPathRegexp := range sudoPaths {
|
|
match := sudoPathRegexp.MatchString(path)
|
|
if match {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|