diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index c50127b17..c2e6bfd6b 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -587,7 +587,7 @@ func TestBackend_CRLs(t *testing.T) { func testFactory(t *testing.T) logical.Backend { b, err := Factory(&logical.BackendConfig{ System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 300 * time.Second, + DefaultLeaseTTLVal: 1000 * time.Second, MaxLeaseTTLVal: 1800 * time.Second, }, StorageView: &logical.InmemStorage{}, @@ -647,6 +647,8 @@ func TestBackend_basic_CA(t *testing.T) { testAccStepCertLease(t, "web", ca, "foo"), testAccStepCertTTL(t, "web", ca, "foo"), testAccStepLogin(t, connState), + testAccStepCertMaxTTL(t, "web", ca, "foo"), + testAccStepLogin(t, connState), testAccStepCertNoLease(t, "web", ca, "foo"), testAccStepLoginDefaultLease(t, connState), testAccStepCert(t, "web", ca, "foo", "*.example.com", "", false), @@ -883,7 +885,7 @@ func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) l Unauthenticated: true, ConnState: &connState, Check: func(resp *logical.Response) error { - if resp.Auth.TTL != 300*time.Second { + if resp.Auth.TTL != 1000*time.Second { t.Fatalf("bad lease length: %#v", resp.Auth) } @@ -1013,6 +1015,21 @@ func testAccStepCertTTL( } } +func testAccStepCertMaxTTL( + t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "certs/" + name, + Data: map[string]interface{}{ + "certificate": string(cert), + "policies": policies, + "display_name": name, + "ttl": "1000s", + "max_ttl": "1200s", + }, + } +} + func testAccStepCertNoLease( t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep { return logicaltest.TestStep{ diff --git a/builtin/credential/cert/path_certs.go b/builtin/credential/cert/path_certs.go index da4598dba..37ed4090c 100644 --- a/builtin/credential/cert/path_certs.go +++ b/builtin/credential/cert/path_certs.go @@ -74,6 +74,19 @@ seconds. Defaults to system/backend default TTL.`, Description: `TTL for tokens issued by this backend. Defaults to system/backend default TTL time.`, }, + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: `Duration in either an integer number of seconds (3600) or +an integer time unit (60m) after which the +issued token can no longer be renewed.`, + }, + "period": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: `If set, indicates that the token generated using this role +should never expire. The token should be renewed within the +duration specified by this value. At each renewal, the token's +TTL will be set to the value of this parameter.`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -131,18 +144,14 @@ func (b *backend) pathCertRead( return nil, nil } - duration := cert.TTL - if duration == 0 { - duration = b.System().DefaultLeaseTTL() - } - return &logical.Response{ Data: map[string]interface{}{ - "certificate": cert.Certificate, - "display_name": cert.DisplayName, - "policies": cert.Policies, - "ttl": duration / time.Second, - "allowed_names": cert.AllowedNames, + "certificate": cert.Certificate, + "display_name": cert.DisplayName, + "policies": cert.Policies, + "ttl": cert.TTL / time.Second, + "max_ttl": cert.MaxTTL / time.Second, + "period": cert.Period / time.Second, }, }, nil } @@ -156,6 +165,47 @@ func (b *backend) pathCertWrite( allowedNames := d.Get("allowed_names").([]string) requiredExtensions := d.Get("required_extensions").([]string) + var resp logical.Response + + // Parse the ttl (or lease duration) + systemDefaultTTL := b.System().DefaultLeaseTTL() + ttl := time.Duration(d.Get("ttl").(int)) * time.Second + if ttl == 0 { + ttl = time.Duration(d.Get("lease").(int)) * time.Second + } + if ttl > systemDefaultTTL { + resp.AddWarning(fmt.Sprintf("Given ttl of %d seconds is greater than current mount/system default of %d seconds", ttl/time.Second, systemDefaultTTL/time.Second)) + } + + if ttl < time.Duration(0) { + return logical.ErrorResponse("ttl cannot be negative"), nil + } + + // Parse max_ttl + systemMaxTTL := b.System().MaxLeaseTTL() + maxTTL := time.Duration(d.Get("max_ttl").(int)) * time.Second + if maxTTL > systemMaxTTL { + resp.AddWarning(fmt.Sprintf("Given max_ttl of %d seconds is greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)) + } + + if maxTTL < time.Duration(0) { + return logical.ErrorResponse("max_ttl cannot be negative"), nil + } + + if maxTTL != 0 && ttl > maxTTL { + return logical.ErrorResponse("ttl should be shorter than max_ttl"), nil + } + + // Parse period + period := time.Duration(d.Get("period").(int)) * time.Second + if period > systemMaxTTL { + resp.AddWarning(fmt.Sprintf("Given period of %d seconds is greater than the backend's maximum TTL of %d seconds", period/time.Second, systemMaxTTL/time.Second)) + } + + if period < time.Duration(0) { + return logical.ErrorResponse("period cannot be negative"), nil + } + // Default the display name to the certificate name if not given if displayName == "" { displayName = name @@ -187,19 +237,9 @@ func (b *backend) pathCertWrite( Policies: policies, AllowedNames: allowedNames, RequiredExtensions: requiredExtensions, - } - - // Parse the lease duration or default to backend/system default - maxTTL := b.System().MaxLeaseTTL() - ttl := time.Duration(d.Get("ttl").(int)) * time.Second - if ttl == time.Duration(0) { - ttl = time.Second * time.Duration(d.Get("lease").(int)) - } - if ttl > maxTTL { - return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", ttl/time.Second, maxTTL/time.Second)), nil - } - if ttl > time.Duration(0) { - certEntry.TTL = ttl + TTL: ttl, + MaxTTL: maxTTL, + Period: period, } // Store it @@ -210,7 +250,12 @@ func (b *backend) pathCertWrite( if err := req.Storage.Put(entry); err != nil { return nil, err } - return nil, nil + + if len(resp.Warnings) == 0 { + return nil, nil + } + + return &resp, nil } type CertEntry struct { @@ -219,6 +264,8 @@ type CertEntry struct { DisplayName string Policies []string TTL time.Duration + MaxTTL time.Duration + Period time.Duration AllowedNames []string RequiredExtensions []string } diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index 71149ed14..2bde0b71b 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/helper/policyutil" @@ -85,9 +86,9 @@ func (b *backend) pathLogin( skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId) akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId) - // Generate a response resp := &logical.Response{ Auth: &logical.Auth{ + Period: matched.Entry.Period, InternalData: map[string]interface{}{ "subject_key_id": skid, "authority_key_id": akid, @@ -109,6 +110,22 @@ func (b *backend) pathLogin( }, }, } + + if matched.Entry.MaxTTL > time.Duration(0) { + // Cap maxTTL to the sysview's max TTL + maxTTL := matched.Entry.MaxTTL + if maxTTL > b.System().MaxLeaseTTL() { + maxTTL = b.System().MaxLeaseTTL() + } + + // Cap TTL to MaxTTL + if resp.Auth.TTL > maxTTL { + resp.AddWarning(fmt.Sprintf("Effective TTL of '%s' exceeded the effective max_ttl of '%s'; TTL value is capped accordingly", (resp.Auth.TTL / time.Second), (maxTTL / time.Second))) + resp.Auth.TTL = maxTTL + } + } + + // Generate a response return resp, nil } @@ -135,7 +152,7 @@ func (b *backend) pathLoginRenew( clientCerts := req.Connection.ConnState.PeerCertificates if len(clientCerts) == 0 { - return nil, fmt.Errorf("no client certificate found") + return logical.ErrorResponse("no client certificate found"), nil } skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId) akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId) @@ -161,7 +178,12 @@ func (b *backend) pathLoginRenew( return nil, fmt.Errorf("policies have changed, not renewing") } - return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d) + resp, err := framework.LeaseExtend(cert.TTL, cert.MaxTTL, b.System())(req, d) + if err != nil { + return nil, err + } + resp.Auth.Period = cert.Period + return resp, nil } func (b *backend) verifyCredentials(req *logical.Request, d *framework.FieldData) (*ParsedCert, *logical.Response, error) { diff --git a/website/source/api/auth/cert/index.html.md b/website/source/api/auth/cert/index.html.md index f94f5f659..d8d2fadac 100644 --- a/website/source/api/auth/cert/index.html.md +++ b/website/source/api/auth/cert/index.html.md @@ -29,21 +29,32 @@ Sets a CA cert and associated parameters in a role name. - `name` `(string: )` - The name of the certificate role. - `certificate` `(string: )` - The PEM-format CA certificate. -- `allowed_names` `(string: "")` - Constrain the Common and Alternative Names in +- `allowed_names` `(string: "")` - Constrain the Common and Alternative Names in the client certificate with a [globbed pattern] - (https://github.com/ryanuber/go-glob/blob/master/README.md#example). Value is - a comma-separated list of patterns. Authentication requires at least one Name matching at least one pattern. If not set, defaults to allowing all names. -- `required_extensions` `(string: "" or array:[])` - Require specific Custom Extension OIDs to exist and match the pattern. - Value is a comma separated string or array of `oid:value`. Expects the extension value to be some type of ASN1 encoded string. - All conditions _must_ be met. Supports globbing on `value`. -- `policies` `(string: "")` - A comma-separated list of policies to set on tokens - issued when authenticating against this CA certificate. -- `display_name` `(string: "")` - The `display_name` to set on tokens issued - when authenticating against this CA certificate. If not set, defaults to the + (https://github.com/ryanuber/go-glob/blob/master/README.md#example). Value is + a comma-separated list of patterns. Authentication requires at least one Name + matching at least one pattern. If not set, defaults to allowing all names. +- `required_extensions` `(string: "" or array:[])` - Require specific Custom + Extension OIDs to exist and match the pattern. Value is a comma separated + string or array of `oid:value`. Expects the extension value to be some type + of ASN1 encoded string. All conditions _must_ be met. Supports globbing on + `value`. +- `policies` `(string: "")` - A comma-separated list of policies to set on + tokens issued when authenticating against this CA certificate. +- `display_name` `(string: "")` - The `display_name` to set on tokens issued + when authenticating against this CA certificate. If not set, defaults to the name of the role. -- `ttl` `(string: "")` - The TTL period of the token, provided as a number of - seconds. If not provided, the token is valid for the the mount or system - default TTL time, in that order. +- `ttl` `(string: "")` - The TTL of the token, provided in either number of + seconds (`3600`) or a time duration (`1h`). If not provided, the token is + valid for the the mount or system default TTL time, in that order. +- `max_ttl` `(string: "")` - Duration in either number of seconds (`3600`) or a + time duration (`1h`) after which the issued token can no longer be renewed. +- `period` `(string: "")` - Duration in either number of seconds (`3600`) or a + time duration (`1h`). If set, the generated token is a periodic token; so long + as it is renewed it never expires unless `max_ttl` is also set, but the TTL + set on the token at each renewal is fixed to the value specified here. If this + value is modified, the token will pick up the new value at its next renewal. + ### Sample Payload @@ -97,7 +108,9 @@ $ curl \ "policies": "", "allowed_names": "", "required_extensions": "", - "ttl": 2764800 + "ttl": 2764800, + "max_ttl": 2764800, + "period": 0 }, "warnings": null, "auth": null