Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allowed_uri_sans_template #10249

Merged
merged 3 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,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) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
Expand Down
43 changes: 25 additions & 18 deletions builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial
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
// match the various toggles set in the role for controlling issuance.
// If one does not pass, it is returned in the string argument.
Expand Down Expand Up @@ -956,15 +979,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn

// validate uri sans
for _, uri := range csr.URIs {
valid := false
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri.String())
if validURI {
valid = true
break
}
}

valid := validateURISAN(b, data, uri.String())
if !valid {
return nil, errutil.UserError{
Err: fmt.Sprintf(
Expand All @@ -986,15 +1001,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
}

for _, uri := range uriAlt {
valid := false
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri)
if validURI {
valid = true
break
}
}

valid := validateURISAN(b, data, uri)
if !valid {
return nil, errutil.UserError{
Err: fmt.Sprintf(
Expand Down
12 changes: 11 additions & 1 deletion builtin/logical/pki/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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{
Expand Down Expand Up @@ -558,6 +565,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
AllowSubdomains: data.Get("allow_subdomains").(bool),
AllowGlobDomains: data.Get("allow_glob_domains").(bool),
AllowAnyName: data.Get("allow_any_name").(bool),
AllowedURISANsTemplate: data.Get("allowed_uri_sans_template").(bool),
EnforceHostnames: data.Get("enforce_hostnames").(bool),
AllowIPSANs: data.Get("allow_ip_sans").(bool),
AllowedURISANs: data.Get("allowed_uri_sans").([]string),
Expand Down Expand Up @@ -783,6 +791,7 @@ type roleEntry struct {
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"`
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"`
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"`
Expand All @@ -804,6 +813,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
"allow_subdomains": r.AllowSubdomains,
"allow_glob_domains": r.AllowGlobDomains,
"allow_any_name": r.AllowAnyName,
"allowed_uri_sans_template": r.AllowedURISANsTemplate,
"enforce_hostnames": r.EnforceHostnames,
"allow_ip_sans": r.AllowIPSANs,
"server_flag": r.ServerFlag,
Expand Down
6 changes: 6 additions & 0 deletions builtin/logical/pki/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ func TestPki_RoleNoStore(t *testing.T) {
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
roleReq.Operation = logical.UpdateOperation
roleReq.Path = "roles/testrole_nostore"
Expand Down
3 changes: 3 additions & 0 deletions changelog/10249.txt
Original file line number Diff line number Diff line change
@@ -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.
```
3 changes: 3 additions & 0 deletions website/content/api-docs/secret/pki.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@ request is denied.
a JSON string slice. Values can contain glob patterns (e.g.
`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
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
Expand Down