diff --git a/builtin/credential/cert/backend.go b/builtin/credential/cert/backend.go index b4b9772d64..1ed45cd4b7 100644 --- a/builtin/credential/cert/backend.go +++ b/builtin/credential/cert/backend.go @@ -28,6 +28,7 @@ func Backend() *backend { }, Paths: append([]*framework.Path{ + pathConfig(&b), pathLogin(&b), pathCerts(&b), pathCRLs(&b), diff --git a/builtin/credential/cert/path_config.go b/builtin/credential/cert/path_config.go new file mode 100644 index 0000000000..9e946c61d2 --- /dev/null +++ b/builtin/credential/cert/path_config.go @@ -0,0 +1,63 @@ +package cert + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfig(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config", + Fields: map[string]*framework.FieldSchema{ + "disable_binding": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: `If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathConfigWrite, + }, + } +} + +func (b *backend) pathConfigWrite( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + disableBinding := data.Get("disable_binding").(bool) + + entry, err := logical.StorageEntryJSON("config", config{ + DisableBinding: disableBinding, + }) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + return nil, nil +} + +// Config returns the configuration for this backend. +func (b *backend) Config(s logical.Storage) (*config, error) { + entry, err := s.Get("config") + if err != nil { + return nil, err + } + + // Returning a default configuration if an entry is not found + var result config + if entry != nil { + if err := entry.DecodeJSON(&result); err != nil { + return nil, fmt.Errorf("error reading configuration: %s", err) + } + } + return &result, nil +} + +type config struct { + DisableBinding bool `json:"disable_binding"` +} diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index 5241cc2818..ad27fb3fb5 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -3,6 +3,7 @@ package cert import ( "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "strings" @@ -29,35 +30,16 @@ func pathLogin(b *backend) *framework.Path { func (b *backend) pathLogin( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - // Get the connection state - if req.Connection == nil || req.Connection.ConnState == nil { - return logical.ErrorResponse("tls connection required"), nil - } - connState := req.Connection.ConnState - // Load the trusted certificates - roots, trusted := b.loadTrustedCerts(req.Storage) - - // Validate the connection state is trusted - trustedChains, err := validateConnState(roots, connState) - if err != nil { + var matched *ParsedCert + if verifyResp, resp, err := b.verifyCredentials(req); err != nil { return nil, err + } else if resp != nil { + return resp, nil + } else { + matched = verifyResp } - // If no trusted chain was found, client is not authenticated - if len(trustedChains) == 0 { - return logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil - } - - validChain := b.checkForValidChain(req.Storage, trustedChains) - if !validChain { - return logical.ErrorResponse( - "no chain containing non-revoked certificates could be found for this login certificate", - ), nil - } - - // Match the trusted chain with the policy - matched := b.matchPolicy(trustedChains, trusted) if matched == nil { return nil, nil } @@ -67,14 +49,25 @@ func (b *backend) pathLogin( ttl = b.System().DefaultLeaseTTL() } + clientCerts := req.Connection.ConnState.PeerCertificates + if len(clientCerts) == 0 { + return logical.ErrorResponse("no client certificate found"), nil + } + skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId) + akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId) + // Generate a response resp := &logical.Response{ Auth: &logical.Auth{ + InternalData: map[string]interface{}{ + "subject_key_id": skid, + "authority_key_id": akid, + }, Policies: matched.Entry.Policies, DisplayName: matched.Entry.DisplayName, Metadata: map[string]string{ "cert_name": matched.Entry.Name, - "common_name": connState.PeerCertificates[0].Subject.CommonName, + "common_name": clientCerts[0].Subject.CommonName, }, LeaseOptions: logical.LeaseOptions{ Renewable: true, @@ -85,6 +78,86 @@ func (b *backend) pathLogin( return resp, nil } +func (b *backend) pathLoginRenew( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + config, err := b.Config(req.Storage) + if err != nil { + return nil, err + } + + if !config.DisableBinding { + var matched *ParsedCert + if verifyResp, resp, err := b.verifyCredentials(req); err != nil { + return nil, err + } else if resp != nil { + return resp, nil + } else { + matched = verifyResp + } + + if matched == nil { + return nil, nil + } + + clientCerts := req.Connection.ConnState.PeerCertificates + if len(clientCerts) == 0 { + return logical.ErrorResponse("no client certificate found"), nil + } + skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId) + akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId) + + // Certificate should not only match a registered certificate policy. + // Also, the identity of the certificate presented should match the identity of the certificate used during login + if req.Auth.InternalData["subject_key_id"] != skid && req.Auth.InternalData["authority_key_id"] != akid { + return logical.ErrorResponse("client identity during renewal not matching client identity used during login"), nil + } + + } + // Get the cert and use its TTL + cert, err := b.Cert(req.Storage, req.Auth.Metadata["cert_name"]) + if err != nil { + return nil, err + } + if cert == nil { + // User no longer exists, do not renew + return nil, nil + } + + return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d) +} + +func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical.Response, error) { + // Get the connection state + if req.Connection == nil || req.Connection.ConnState == nil { + return nil, logical.ErrorResponse("tls connection required"), nil + } + connState := req.Connection.ConnState + + // Load the trusted certificates + roots, trusted := b.loadTrustedCerts(req.Storage) + + // Validate the connection state is trusted + trustedChains, err := validateConnState(roots, connState) + if err != nil { + return nil, nil, err + } + + // If no trusted chain was found, client is not authenticated + if len(trustedChains) == 0 { + return nil, logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil + } + + validChain := b.checkForValidChain(req.Storage, trustedChains) + if !validChain { + return nil, logical.ErrorResponse( + "no chain containing non-revoked certificates could be found for this login certificate", + ), nil + } + + // Match the trusted chain with the policy + return b.matchPolicy(trustedChains, trusted), nil, nil +} + // matchPolicy is used to match the associated policy with the certificate that // was used to establish the client identity. func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert { @@ -204,18 +277,3 @@ func validateConnState(roots *x509.CertPool, cs *tls.ConnectionState) ([][]*x509 } return chains, nil } - -func (b *backend) pathLoginRenew( - req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - // Get the cert and use its TTL - cert, err := b.Cert(req.Storage, req.Auth.Metadata["cert_name"]) - if err != nil { - return nil, err - } - if cert == nil { - // User no longer exists, do not renew - return nil, nil - } - - return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d) -} diff --git a/builtin/logical/ssh/path_roles.go b/builtin/logical/ssh/path_roles.go index 0103a6701d..6d673760ef 100644 --- a/builtin/logical/ssh/path_roles.go +++ b/builtin/logical/ssh/path_roles.go @@ -83,7 +83,7 @@ func pathRoles(b *backend) *framework.Path { Description: ` [Optional for both types] Comma separated list of CIDR blocks for which the role is applicable for. - CIDR blocks can belong to more than one role. Defaults to zero-address (0.0.0.0/0)`, + CIDR blocks can belong to more than one role.`, }, "exclude_cidr_list": &framework.FieldSchema{ Type: framework.TypeString, diff --git a/vault/expiration.go b/vault/expiration.go index dd4026e3ff..3e06d2591f 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -309,7 +309,7 @@ func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*log // RenewToken is used to renew a token which does not need to // invoke a logical backend. -func (m *ExpirationManager) RenewToken(source string, token string, +func (m *ExpirationManager) RenewToken(req *logical.Request, source string, token string, increment time.Duration) (*logical.Auth, error) { defer metrics.MeasureSince([]string{"expire", "renew-token"}, time.Now()) // Compute the Lease ID @@ -327,7 +327,7 @@ func (m *ExpirationManager) RenewToken(source string, token string, } // Attempt to renew the auth entry - resp, err := m.renewAuthEntry(le, increment) + resp, err := m.renewAuthEntry(req, le, increment) if err != nil { return nil, err } @@ -550,14 +550,15 @@ func (m *ExpirationManager) renewEntry(le *leaseEntry, increment time.Duration) } // renewAuthEntry is used to attempt renew of an auth entry -func (m *ExpirationManager) renewAuthEntry(le *leaseEntry, increment time.Duration) (*logical.Response, error) { +func (m *ExpirationManager) renewAuthEntry(req *logical.Request, le *leaseEntry, increment time.Duration) (*logical.Response, error) { auth := *le.Auth auth.IssueTime = le.IssueTime auth.Increment = increment auth.ClientToken = "" - req := logical.RenewAuthRequest(le.Path, &auth, nil) - resp, err := m.router.Route(req) + authReq := logical.RenewAuthRequest(le.Path, &auth, nil) + authReq.Connection = req.Connection + resp, err := m.router.Route(authReq) if err != nil { return nil, fmt.Errorf("failed to renew entry: %v", err) } diff --git a/vault/router.go b/vault/router.go index 51353b3430..5ade3d0a07 100644 --- a/vault/router.go +++ b/vault/router.go @@ -222,9 +222,6 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica } } - // Determine if this path is an unauthenticated path before we modify it - loginPath := r.LoginPath(req.Path) - // Adjust the path to exclude the routing prefix original := req.Path req.Path = strings.TrimPrefix(req.Path, mount) @@ -248,11 +245,8 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica req.ClientToken = re.SaltID(req.ClientToken) } - // If the request is not a login path, then clear the connection + // Cache the pointer to the original connection object originalConn := req.Connection - if !loginPath { - req.Connection = nil - } // Reset the request before returning defer func() { diff --git a/vault/token_store.go b/vault/token_store.go index 84567855b6..0e8e5dcff9 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -895,7 +895,7 @@ func (ts *TokenStore) handleRenew( } // Renew the token and its children - auth, err := ts.expiration.RenewToken(te.Path, te.ID, increment) + auth, err := ts.expiration.RenewToken(req, te.Path, te.ID, increment) if err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } diff --git a/website/source/docs/auth/cert.html.md b/website/source/docs/auth/cert.html.md index 0fcc0f9db1..d43391ee86 100644 --- a/website/source/docs/auth/cert.html.md +++ b/website/source/docs/auth/cert.html.md @@ -375,3 +375,36 @@ of the header should be "X-Vault-Token" and the value should be the token. + +### /auth/cert/config + +#### POST + +
+
Description
+
+ Configuration options for the backend. +
+ +
Method
+
POST
+ +
URL
+
`/auth/cert/config`
+ +
Parameters
+
+ +
+ +
Returns
+
+ A `204` response code. +
+