Deprecate lease -> ttl in PKI backend, and default to system TTL values if not given. This prevents issuing certificates with a longer duration than the maximum lease TTL configured in Vault. Fixes #470.

This commit is contained in:
Jeff Mitchell 2015-08-27 12:24:37 -07:00
parent eed9b6da7f
commit a4fc4a8e90
5 changed files with 183 additions and 63 deletions

View File

@ -23,7 +23,16 @@ var (
// Performs basic tests on CA functionality
func TestBackend_basic(t *testing.T) {
b := Backend()
b, err := Factory(&logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: time.Hour * 24,
MaxLeaseTTLVal: time.Hour * 24 * 30,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
testCase := logicaltest.TestCase{
Backend: b,
@ -40,7 +49,16 @@ func TestBackend_basic(t *testing.T) {
// Generates and tests steps that walk through the various possibilities
// of role flags to ensure that they are properly restricted
func TestBackend_roles(t *testing.T) {
b := Backend()
b, err := Factory(&logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: time.Hour * 24,
MaxLeaseTTLVal: time.Hour * 24 * 30,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
testCase := logicaltest.TestCase{
Backend: b,
@ -111,7 +129,7 @@ func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Dura
}
if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 {
return nil, fmt.Errorf("Validity period too large")
return nil, fmt.Errorf("Validity period of %d too large vs max of 10", cert.NotAfter.Unix())
}
return parsedCertBundle, nil
@ -204,7 +222,7 @@ func generateCASteps(t *testing.T) []logicaltest.TestStep {
// Generates steps to test out various role permutations
func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
roleVals := roleEntry{
LeaseMax: "12h",
MaxTTL: "12h",
}
issueVals := certutil.IssueData{}
ret := []logicaltest.TestStep{}
@ -324,7 +342,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
issueTestStep.ErrorOk = true
}
validity, _ := time.ParseDuration(roleVals.LeaseMax)
validity, _ := time.ParseDuration(roleVals.MaxTTL)
addTests(getCnCheck(name, roleVals.KeyType, usage, validity))
}
}
@ -388,11 +406,11 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
{
roleTestStep.ErrorOk = true
roleVals.Lease = ""
roleVals.LeaseMax = ""
roleVals.MaxTTL = ""
addTests(nil)
roleVals.Lease = "12h"
roleVals.LeaseMax = "6h"
roleVals.MaxTTL = "6h"
addTests(nil)
roleTestStep.ErrorOk = false

View File

@ -34,7 +34,7 @@ type certCreationBundle struct {
IPSANs []net.IP
KeyType string
KeyBits int
Lease time.Duration
TTL time.Duration
Usage certUsage
}
@ -234,7 +234,7 @@ func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBu
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(creationInfo.Lease),
NotAfter: time.Now().Add(creationInfo.TTL),
KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement),
BasicConstraintsValid: true,
IsCA: false,

View File

@ -39,7 +39,14 @@ common-delimited list`,
},
"lease": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The requested lease",
Description: `The requested lease. DEPRECATED: use "ttl" instead.`,
},
"ttl": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested Time To Live for the certificate;
sets the expiration date. If not specified
the role default TTL it used. Cannot be larer
than the role max TTL.`,
},
},
@ -97,24 +104,44 @@ func (b *backend) pathIssueCert(
}
}
leaseField := data.Get("lease").(string)
if len(leaseField) == 0 {
leaseField = role.Lease
ttlField := data.Get("ttl").(string)
if len(ttlField) == 0 {
ttlField = data.Get("lease").(string)
if len(ttlField) == 0 {
ttlField = role.TTL
}
}
lease, err := time.ParseDuration(leaseField)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid requested lease: %s", err)), nil
}
leaseMax, err := time.ParseDuration(role.LeaseMax)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
var ttl time.Duration
if len(ttlField) == 0 {
ttl = b.System.DefaultLeaseTTL()
} else {
ttl, err = time.ParseDuration(ttlField)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid requested ttl: %s", err)), nil
}
}
if lease > leaseMax {
return logical.ErrorResponse("Lease expires after maximum allowed by this role"), nil
var maxTTL time.Duration
if len(role.MaxTTL) == 0 {
maxTTL = b.System.MaxLeaseTTL()
} else {
maxTTL, err = time.ParseDuration(role.MaxTTL)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid ttl: %s", err)), nil
}
}
if ttl > maxTTL {
// Don't error if they were using system defaults, only error if
// they specifically chose a bad TTL
if len(ttlField) == 0 {
ttl = maxTTL
} else {
return logical.ErrorResponse("TTL is larger than maximum allowed by this role"), nil
}
}
badName, err := validateCommonNames(req, commonNames, role)
@ -132,8 +159,8 @@ func (b *backend) pathIssueCert(
return nil, fmt.Errorf("Error fetching CA certificate: %s", caErr)
}
if time.Now().Add(lease).After(signingBundle.Certificate.NotAfter) {
return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil
if time.Now().Add(ttl).After(signingBundle.Certificate.NotAfter) {
return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as TTL is beyond the expiration of the CA certificate")), nil
}
var usage certUsage
@ -154,7 +181,7 @@ func (b *backend) pathIssueCert(
IPSANs: ipSANs,
KeyType: role.KeyType,
KeyBits: role.KeyBits,
Lease: lease,
TTL: ttl,
Usage: usage,
}
@ -177,7 +204,7 @@ func (b *backend) pathIssueCert(
"serial_number": cb.SerialNumber,
})
resp.Secret.TTL = lease
resp.Secret.TTL = ttl
err = req.Storage.Put(&logical.StorageEntry{
Key: "certs/" + cb.SerialNumber,

View File

@ -24,13 +24,29 @@ func pathRoles(b *backend) *framework.Path {
Description: `The lease length if no specific lease length is
requested. The lease length controls the expiration
of certificates issued by this backend. Defaults to
the value of lease_max.`,
the value of lease_max. DEPRECATED: use "ttl" instead.`,
},
"lease_max": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: `The maximum allowed lease length.
DEPRECATED: use "ttl" instead.`,
},
"ttl": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: `The lease duration if no specific lease duration is
requested. The lease duration controls the expiration
of certificates issued by this backend. Defaults to
the value of max_ttl.`,
},
"max_ttl": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "The maximum allowed lease length",
Description: "The maximum allowed lease duration",
},
"allow_localhost": &framework.FieldSchema{
@ -150,6 +166,28 @@ func (b *backend) getRole(s logical.Storage, n string) (*roleEntry, error) {
return nil, err
}
// Migrate existing saved entries and save back if changed
modified := false
if len(result.TTL) == 0 && len(result.Lease) != 0 {
result.TTL = result.Lease
result.Lease = ""
modified = true
}
if len(result.MaxTTL) == 0 && len(result.LeaseMax) != 0 {
result.MaxTTL = result.LeaseMax
result.LeaseMax = ""
modified = true
}
if modified {
jsonEntry, err := logical.StorageEntryJSON("role/"+n, &result)
if err != nil {
return nil, err
}
if err := s.Put(jsonEntry); err != nil {
return nil, err
}
}
return &result, nil
}
@ -173,6 +211,19 @@ func (b *backend) pathRoleRead(
return nil, nil
}
hasMax := true
if len(role.MaxTTL) == 0 {
role.MaxTTL = "(system default)"
hasMax = false
}
if len(role.TTL) == 0 {
if hasMax {
role.TTL = "(system default, capped to role max)"
} else {
role.TTL = "(system default)"
}
}
resp := &logical.Response{
Data: structs.New(role).Map(),
}
@ -182,11 +233,12 @@ func (b *backend) pathRoleRead(
func (b *backend) pathRoleCreate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
name := data.Get("name").(string)
entry := &roleEntry{
LeaseMax: data.Get("lease_max").(string),
Lease: data.Get("lease").(string),
MaxTTL: data.Get("max_ttl").(string),
TTL: data.Get("ttl").(string),
AllowLocalhost: data.Get("allow_localhost").(bool),
AllowedBaseDomain: data.Get("allowed_base_domain").(string),
AllowTokenDisplayName: data.Get("allow_token_displayname").(bool),
@ -201,27 +253,45 @@ func (b *backend) pathRoleCreate(
KeyBits: data.Get("key_bits").(int),
}
if len(entry.LeaseMax) == 0 {
return logical.ErrorResponse("\"lease_max\" value must be supplied"), nil
if len(entry.MaxTTL) == 0 {
entry.MaxTTL = data.Get("lease_max").(string)
}
leaseMax, err := time.ParseDuration(entry.LeaseMax)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
}
switch len(entry.Lease) {
case 0:
entry.Lease = entry.LeaseMax
default:
lease, err := time.ParseDuration(entry.Lease)
var maxTTL time.Duration
if len(entry.MaxTTL) == 0 {
maxTTL = b.System.MaxLeaseTTL()
} else {
maxTTL, err = time.ParseDuration(entry.MaxTTL)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
"Invalid ttl: %s", err)), nil
}
if lease > leaseMax {
return logical.ErrorResponse("\"lease\" value must be less than \"lease_max\" value"), nil
}
if maxTTL > b.System.MaxLeaseTTL() {
return logical.ErrorResponse("Requested max TTL is higher than system maximum"), nil
}
var ttl time.Duration
if len(entry.TTL) == 0 {
entry.TTL = data.Get("lease").(string)
}
switch len(entry.TTL) {
case 0:
ttl = b.System.DefaultLeaseTTL()
default:
ttl, err = time.ParseDuration(entry.TTL)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid ttl: %s", err)), nil
}
}
if ttl > maxTTL {
// If they are using the system default, cap it to the role max;
// if it was specified on the command line, make it an error
if len(entry.TTL) == 0 {
ttl = maxTTL
} else {
return logical.ErrorResponse("\"ttl\" value must be less than \"max_ttl\" and/or system default max lease TTL value"), nil
}
}
@ -262,6 +332,8 @@ func (b *backend) pathRoleCreate(
type roleEntry struct {
LeaseMax string `json:"lease_max" structs:"lease_max" mapstructure:"lease_max"`
Lease string `json:"lease" structs:"lease" mapstructure:"lease"`
MaxTTL string `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`
TTL string `json:"ttl" structs:"ttl" mapstructure:"ttl"`
AllowLocalhost bool `json:"allow_localhost" structs:"allow_localhost" mapstructure:"allow_localhost"`
AllowedBaseDomain string `json:"allowed_base_domain" structs:"allowed_base_domain" mapstructure:"allowed_base_domain"`
AllowTokenDisplayName bool `json:"allow_token_displayname" structs:"allow_token_displayname" mapstructure:"allow_token_displayname"`

View File

@ -12,7 +12,7 @@ Name: `pki`
The PKI secret backend for Vault generates X.509 certificates dynamically based on configured roles. This means services can get certificates needed for both client and server authentication without going through the usual manual process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process to complete. Vault's built-in authentication and authorization mechanisms provide the verification functionality.
By keeping leases relatively short, revocations are less likely to be needed, keeping CRLs short and helping the backend scale to large workloads. This in turn allows each instance of a running application to have a unique certificate, eliminating sharing and the accompanying pain of revocation and rollover.
By keeping TTLs relatively short, revocations are less likely to be needed, keeping CRLs short and helping the backend scale to large workloads. This in turn allows each instance of a running application to have a unique certificate, eliminating sharing and the accompanying pain of revocation and rollover.
In addition, by allowing revocation to mostly be forgone, this backend allows for ephemeral certificates; certificates can be fetched and stored in memory upon application startup and discarded upon shutdown, without ever being written to disk.
@ -92,7 +92,7 @@ The next step is to configure a role. A role is a logical name that maps to a po
```text
$ vault write pki/roles/example-dot-com \
allowed_base_domain="example.com" \
allow_subdomains="true" lease_max="72h"
allow_subdomains="true" max_ttl="72h"
Success! Data written to: pki/roles/example-dot-com
```
@ -370,11 +370,12 @@ If you get stuck at any time, simply run `vault path-help pki` or with a subpath
default).
</li>
<li>
<span class="param">lease</span>
<span class="param">ttl</span>
<span class="param-flags">optional</span>
Requested lease time. Cannot be greater than the role's
`lease_max` parameter. If not provided, the role's `lease`
value will be used.
Requested Time To Live. Cannot be greater than the role's
`max_ttl` value. If not provided, the role's `ttl`
value will be used. Note that the role values default
to system values if not explicitly set.
</li>
</ul>
</dd>
@ -470,17 +471,19 @@ If you get stuck at any time, simply run `vault path-help pki` or with a subpath
<dd>
<ul>
<li>
<span class="param">lease</span>
<span class="param">ttl</span>
<span class="param-flags">optional</span>
The lease value provided as a string duration
The Time To Live value provided as a string duration
with time suffix. Hour is the largest suffix.
If not set, uses the value of `lease_max`.
If not set, uses the system default value or the
value of `max_ttl`, whichever is shorter.
</li>
<li>
<span class="param">lease_max</span>
<span class="param-flags">required</span>
The maximum lease value provided as a string duration
with time suffix. Hour is the largest suffix.
<span class="param">max_ttl</span>
<span class="param-flags">optional</span>
The maximum Time To Live provided as a string duration
with time suffix. Hour is the largest suffix. If not set,
defaults to the system maximum lease TTL.
</li>
<li>
<span class="param">allow_localhost</span>
@ -611,8 +614,8 @@ If you get stuck at any time, simply run `vault path-help pki` or with a subpath
"code_signing_flag": false,
"key_bits": 2048,
"key_type": "rsa",
"lease": "6h",
"lease_max": "12h",
"ttl": "6h",
"max_ttl": "12h",
"server_flag": true
}
}