mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-08 16:24:51 -04:00
Merge pull request #1127 from hashicorp/iss1000-cert-renewal
Cert: renewal enhancements
This commit is contained in:
commit
f40cfbeabd
8 changed files with 205 additions and 55 deletions
|
|
@ -28,6 +28,7 @@ func Backend() *backend {
|
|||
},
|
||||
|
||||
Paths: append([]*framework.Path{
|
||||
pathConfig(&b),
|
||||
pathLogin(&b),
|
||||
pathCerts(&b),
|
||||
pathCRLs(&b),
|
||||
|
|
|
|||
63
builtin/credential/cert/path_config.go
Normal file
63
builtin/credential/cert/path_config.go
Normal file
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,3 +375,36 @@ of the header should be "X-Vault-Token" and the value should be the token.
|
|||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /auth/cert/config
|
||||
|
||||
#### POST
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Configuration options for the backend.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>POST</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/auth/cert/config`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="param">disable_binding</span>
|
||||
<span class="param-flags">optional</span>
|
||||
If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
A `204` response code.
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
|||
Loading…
Reference in a new issue