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:
parent
cbdea53dd7
commit
ccc1098ea3
|
@ -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{
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue