Add allowed_uri_sans_template (#10249)

* Add allowed_uri_sans_template

Enables identity templating for the allowed_uri_sans field in PKI cert roles.

Implemented as suggested in #8509

* changelog++

* Update docs with URI SAN templating
This commit is contained in:
Pete Bohman 2021-12-15 10:18:28 -05:00 committed by GitHub
parent cbdea53dd7
commit ccc1098ea3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 19 deletions

View file

@ -2989,6 +2989,128 @@ func TestBackend_URI_SANs(t *testing.T) {
} }
} }
func TestBackend_AllowedURISANsTemplate(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Write test policy for userpass auth method.
err := client.Sys().PutPolicy("test", `
path "pki/*" {
capabilities = ["update"]
}`)
if err != nil {
t.Fatal(err)
}
// Enable userpass auth method.
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}
// Configure test role for userpass.
if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{
"password": "test",
"policies": "test",
}); err != nil {
t.Fatal(err)
}
// Login userpass for test role and keep client token.
secret, err := client.Logical().Write("auth/userpass/login/userpassname", map[string]interface{}{
"password": "test",
})
if err != nil || secret == nil {
t.Fatal(err)
}
userpassToken := secret.Auth.ClientToken
// Get auth accessor for identity template.
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
userpassAccessor := auths["userpass/"].Accessor
// Mount PKI.
err = client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "60h",
},
})
if err != nil {
t.Fatal(err)
}
// Generate internal CA.
_, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}
// Write role PKI.
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
"allowed_uri_sans": []string{"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}",
"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}/*", "spiffe://domain/foo"},
"allowed_uri_sans_template": true,
"require_cn": false,
})
if err != nil {
t.Fatal(err)
}
// Issue certificate with identity templating
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname, spiffe://domain/foo"})
if err != nil {
t.Fatal(err)
}
// Issue certificate with identity templating and glob
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname/bar"})
if err != nil {
t.Fatal(err)
}
// Issue certificate with non-matching identity template parameter
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/unknownuser"})
if err == nil {
t.Fatal(err)
}
// Set allowed_uri_sans_template to false.
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
"allowed_uri_sans_template": false,
})
if err != nil {
t.Fatal(err)
}
// Issue certificate with userpassToken.
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/users/userpassname"})
if err == nil {
t.Fatal("expected error")
}
}
func TestBackend_AllowedDomainsTemplate(t *testing.T) { func TestBackend_AllowedDomainsTemplate(t *testing.T) {
coreConfig := &vault.CoreConfig{ coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{ CredentialBackends: map[string]logical.Factory{

View file

@ -176,6 +176,29 @@ func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial
return certEntry, nil return certEntry, nil
} }
// Given a URI SAN, verify that it is allowed.
func validateURISAN(b *backend, data *inputBundle, uri string) bool {
valid := false
for _, allowed := range data.role.AllowedURISANs {
if data.role.AllowedURISANsTemplate {
isTemplate, _ := framework.ValidateIdentityTemplate(allowed)
if isTemplate && data.req.EntityID != "" {
tmpAllowed, err := framework.PopulateIdentityTemplate(allowed, data.req.EntityID, b.System())
if err != nil {
continue
}
allowed = tmpAllowed
}
}
validURI := glob.Glob(allowed, uri)
if validURI {
valid = true
break
}
}
return valid
}
// Given a set of requested names for a certificate, verifies that all of them // Given a set of requested names for a certificate, verifies that all of them
// match the various toggles set in the role for controlling issuance. // match the various toggles set in the role for controlling issuance.
// If one does not pass, it is returned in the string argument. // If one does not pass, it is returned in the string argument.
@ -956,15 +979,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
// validate uri sans // validate uri sans
for _, uri := range csr.URIs { for _, uri := range csr.URIs {
valid := false valid := validateURISAN(b, data, uri.String())
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri.String())
if validURI {
valid = true
break
}
}
if !valid { if !valid {
return nil, errutil.UserError{ return nil, errutil.UserError{
Err: fmt.Sprintf( Err: fmt.Sprintf(
@ -986,15 +1001,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
} }
for _, uri := range uriAlt { for _, uri := range uriAlt {
valid := false valid := validateURISAN(b, data, uri)
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri)
if validURI {
valid = true
break
}
}
if !valid { if !valid {
return nil, errutil.UserError{ return nil, errutil.UserError{
Err: fmt.Sprintf( Err: fmt.Sprintf(

View file

@ -144,7 +144,14 @@ Any valid URI is accepted, these values support globbing.`,
}, },
}, },
"allowed_other_sans": { "allowed_uri_sans_template": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `If set, Allowed URI SANs can be specified using identity template policies.
Non-templated URI SANs are also permitted.`,
Default: false,
},
"allowed_other_sans": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice, Type: framework.TypeCommaStringSlice,
Description: `If set, an array of allowed other names to put in SANs. These values support globbing and must be in the format <oid>;<type>:<value>. Currently only "utf8" is a valid type. All values, including globbing values, must use this syntax, with the exception being a single "*" which allows any OID and any value (but type must still be utf8).`, Description: `If set, an array of allowed other names to put in SANs. These values support globbing and must be in the format <oid>;<type>:<value>. Currently only "utf8" is a valid type. All values, including globbing values, must use this syntax, with the exception being a single "*" which allows any OID and any value (but type must still be utf8).`,
DisplayAttrs: &framework.DisplayAttributes{ DisplayAttrs: &framework.DisplayAttributes{
@ -565,6 +572,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
AllowSubdomains: data.Get("allow_subdomains").(bool), AllowSubdomains: data.Get("allow_subdomains").(bool),
AllowGlobDomains: data.Get("allow_glob_domains").(bool), AllowGlobDomains: data.Get("allow_glob_domains").(bool),
AllowAnyName: data.Get("allow_any_name").(bool), AllowAnyName: data.Get("allow_any_name").(bool),
AllowedURISANsTemplate: data.Get("allowed_uri_sans_template").(bool),
EnforceHostnames: data.Get("enforce_hostnames").(bool), EnforceHostnames: data.Get("enforce_hostnames").(bool),
AllowIPSANs: data.Get("allow_ip_sans").(bool), AllowIPSANs: data.Get("allow_ip_sans").(bool),
AllowedURISANs: data.Get("allowed_uri_sans").([]string), AllowedURISANs: data.Get("allowed_uri_sans").([]string),
@ -783,6 +791,7 @@ type roleEntry struct {
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"` AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"` AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"`
AllowedURISANs []string `json:"allowed_uri_sans" mapstructure:"allowed_uri_sans"` AllowedURISANs []string `json:"allowed_uri_sans" mapstructure:"allowed_uri_sans"`
AllowedURISANsTemplate bool `json:"allowed_uri_sans_template"`
PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"` PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"`
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"` ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"`
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"` BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"`
@ -804,6 +813,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
"allow_subdomains": r.AllowSubdomains, "allow_subdomains": r.AllowSubdomains,
"allow_glob_domains": r.AllowGlobDomains, "allow_glob_domains": r.AllowGlobDomains,
"allow_any_name": r.AllowAnyName, "allow_any_name": r.AllowAnyName,
"allowed_uri_sans_template": r.AllowedURISANsTemplate,
"enforce_hostnames": r.EnforceHostnames, "enforce_hostnames": r.EnforceHostnames,
"allow_ip_sans": r.AllowIPSANs, "allow_ip_sans": r.AllowIPSANs,
"server_flag": r.ServerFlag, "server_flag": r.ServerFlag,

View file

@ -592,6 +592,12 @@ func TestPki_RoleNoStore(t *testing.T) {
t.Fatalf("allowed_domains_template should not be set by default") t.Fatalf("allowed_domains_template should not be set by default")
} }
// By default, allowed_uri_sans_template should be `false`
allowedURISANsTemplate := resp.Data["allowed_uri_sans_template"].(bool)
if allowedURISANsTemplate {
t.Fatalf("allowed_uri_sans_template should not be set by default")
}
// Make sure that setting no_store to `true` works properly // Make sure that setting no_store to `true` works properly
roleReq.Operation = logical.UpdateOperation roleReq.Operation = logical.UpdateOperation
roleReq.Path = "roles/testrole_nostore" roleReq.Path = "roles/testrole_nostore"

3
changelog/10249.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
secrets/pki: Allow URI SAN templates in allowed_uri_sans when allowed_uri_sans_template is set to true.
```

View file

@ -793,6 +793,9 @@ request is denied.
a JSON string slice. Values can contain glob patterns (e.g. a JSON string slice. Values can contain glob patterns (e.g.
`spiffe://hostname/*`). `spiffe://hostname/*`).
- `allowed_uri_sans_template` `()bool: false)`  When set, `allowed_uri_sans`
may contain templates, as with [ACL Path Templating](/docs/concepts/policies).
- `allowed_other_sans` `(string: "")`  Defines allowed custom OID/UTF8-string - `allowed_other_sans` `(string: "")`  Defines allowed custom OID/UTF8-string
SANs. This can be a comma-delimited list or a JSON string slice, where SANs. This can be a comma-delimited list or a JSON string slice, where
each element has the same format as OpenSSL: `<oid>;<type>:<value>`, but each element has the same format as OpenSSL: `<oid>;<type>:<value>`, but