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

fix bug with allowed_users_template and add allowed_domains_template for SSH role #16056

Merged
merged 2 commits into from
Aug 16, 2022
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
59 changes: 47 additions & 12 deletions builtin/logical/ssh/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,31 @@ func TestBackend_AllowedUsers(t *testing.T) {
}
}

func TestBackend_AllowedDomainsTemplate(t *testing.T) {
testAllowedDomainsTemplate := "{{ identity.entity.metadata.ssh_username }}.example.com"
expectedValidPrincipal := "foo." + testUserName + ".example.com"
testAllowedPrincipalsTemplate(
t, testAllowedDomainsTemplate,
expectedValidPrincipal,
map[string]string{
"ssh_username": testUserName,
},
map[string]interface{}{
"key_type": testCaKeyType,
"algorithm_signer": "rsa-sha2-256",
"allow_host_certificates": true,
"allow_subdomains": true,
"allowed_domains": testAllowedDomainsTemplate,
"allowed_domains_template": true,
},
map[string]interface{}{
"cert_type": "host",
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
},
)
}

func TestBackend_AllowedUsersTemplate(t *testing.T) {
testAllowedUsersTemplate(t,
"{{ identity.entity.metadata.ssh_username }}",
Expand Down Expand Up @@ -2093,9 +2118,9 @@ func testDefaultUserTemplate(t *testing.T, testDefaultUserTemplate string,
}
}

func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
func testAllowedPrincipalsTemplate(t *testing.T, testAllowedDomainsTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string,
) {
roleConfigPayload map[string]interface{}, signingPayload map[string]interface{}) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup()
client := cluster.Cores[0].Client
Expand All @@ -2115,22 +2140,14 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
t.Fatal(err)
}

_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"allowed_users": testAllowedUsersTemplate,
"allowed_users_template": true,
})
_, err = client.Logical().Write("ssh/roles/my-role", roleConfigPayload)
if err != nil {
t.Fatal(err)
}

// sign SSH key as userpass user
client.SetToken(userpassToken)
signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
})
signResponse, err := client.Logical().Write("ssh/sign/my-role", signingPayload)
if err != nil {
t.Fatal(err)
}
Expand All @@ -2151,6 +2168,24 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
}
}

func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string) {
testAllowedPrincipalsTemplate(
t, testAllowedUsersTemplate,
expectedValidPrincipal, testEntityMetadata,
map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"allowed_users": testAllowedUsersTemplate,
"allowed_users_template": true,
},
map[string]interface{}{
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
},
)
}

func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Expand Down
8 changes: 4 additions & 4 deletions builtin/logical/ssh/path_issue_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic

var parsedPrincipals []string
if certificateType == ssh.HostCert {
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, validateValidPrincipalForHosts(role))
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, role.AllowedDomainsTemplate, validateValidPrincipalForHosts(role))
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
Expand All @@ -77,7 +77,7 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
return nil, err
}
}
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, strutil.StrListContains)
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, role.AllowedUsersTemplate, strutil.StrListContains)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
Expand Down Expand Up @@ -160,7 +160,7 @@ func (b *backend) renderPrincipal(principal string, req *logical.Request) (strin
return principal, nil
}

func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, role *sshRole, defaultPrincipal, principalsAllowedByRole string, validatePrincipal func([]string, string) bool) ([]string, error) {
func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, role *sshRole, defaultPrincipal, principalsAllowedByRole string, enableTemplating bool, validatePrincipal func([]string, string) bool) ([]string, error) {
validPrincipals := ""
validPrincipalsRaw, ok := data.GetOk("valid_principals")
if ok {
Expand All @@ -173,7 +173,7 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic
// Build list of allowed Principals from template and static principalsAllowedByRole
var allowedPrincipals []string
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
if role.AllowedUsersTemplate {
if enableTemplating {
rendered, err := b.renderPrincipal(principal, req)
if err != nil {
return nil, err
Expand Down
12 changes: 12 additions & 0 deletions builtin/logical/ssh/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type sshRole struct {
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"`
AllowedDomainsTemplate bool `mapstructure:"allowed_domains_template" json:"allowed_domains_template"`
KeyOptionSpecs string `mapstructure:"key_option_specs" json:"key_option_specs"`
MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"`
TTL string `mapstructure:"ttl" json:"ttl"`
Expand Down Expand Up @@ -223,6 +224,15 @@ func pathRoles(b *backend) *framework.Path {
valid host. If only certain domains are allowed, then this list enforces it.
`,
},
"allowed_domains_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
If set, Allowed domains can be specified using identity template policies.
Non-templated domains are also permitted.
`,
Default: false,
},
"key_option_specs": {
Type: framework.TypeString,
Description: `
Expand Down Expand Up @@ -567,6 +577,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser, signer string, data *f
AllowedUsers: allowedUsers,
AllowedUsersTemplate: data.Get("allowed_users_template").(bool),
AllowedDomains: data.Get("allowed_domains").(string),
AllowedDomainsTemplate: data.Get("allowed_domains_template").(bool),
DefaultUser: defaultUser,
DefaultUserTemplate: data.Get("default_user_template").(bool),
AllowBareDomains: data.Get("allow_bare_domains").(bool),
Expand Down Expand Up @@ -750,6 +761,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"allowed_users": role.AllowedUsers,
"allowed_users_template": role.AllowedUsersTemplate,
"allowed_domains": role.AllowedDomains,
"allowed_domains_template": role.AllowedDomainsTemplate,
"default_user": role.DefaultUser,
"default_user_template": role.DefaultUserTemplate,
"ttl": int64(ttl.Seconds()),
Expand Down
3 changes: 3 additions & 0 deletions changelog/16056.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Add allowed_domains_template to allow templating of allowed_domains.
```
6 changes: 5 additions & 1 deletion website/content/api-docs/secret/ssh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,18 @@ This endpoint creates or updates a named role.
Use with caution. N.B.: if the type is `ca`, an empty list does not allow any user;
instead you must use `*` to enable this behavior.

- `allowed_users_template` `(bool: false)` - If set, allowed_users can be specified
- `allowed_users_template` `(bool: false)` - If set, `allowed_users` can be specified
using identity template policies. Non-templated users are also permitted.

- `allowed_domains` `(string: "")` – The list of domains for which a client can
request a host certificate. If this option is explicitly set to `"*"`, then
credentials can be created for any domain. See also `allow_bare_domains` and
`allow_subdomains`.

- `allowed_domains_template` `(bool: false)` - If set, `allowed_domains` can be
specified using identity template policies. Non-templated domains are also
permitted.

- `key_option_specs` `(string: "")` – Specifies a comma separated option
specification which will be prefixed to RSA keys in the remote host's
authorized_keys file. N.B.: Vault does not check this string for validity.
Expand Down