Skip to content

Commit

Permalink
Allow identity templates in ssh backend default_user field
Browse files Browse the repository at this point in the history
  • Loading branch information
ianferguson committed Jul 19, 2022
1 parent c67e009 commit 3aa20e0
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 17 deletions.
201 changes: 201 additions & 0 deletions builtin/logical/ssh/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,147 @@ func TestBackend_AllowedUsersTemplate_WithStaticPrefix(t *testing.T) {
)
}

func TestBackend_DefaultUserTemplate(t *testing.T) {
testDefaultUserTemplate(t,
"{{ identity.entity.metadata.ssh_username }}",
testUserName,
map[string]string{
"ssh_username": testUserName,
},
)
}

func TestBackend_DefaultUserTemplate_WithStaticPrefix(t *testing.T) {
testDefaultUserTemplate(t,
"user-{{ identity.entity.metadata.ssh_username }}",
"user-"+testUserName,
map[string]string{
"ssh_username": testUserName,
},
)
}

func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateTrue(t *testing.T) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup()
client := cluster.Cores[0].Client

// set metadata "ssh_username" to userpass username
tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{
"token": userpassToken,
})
if err != nil {
t.Fatal(err)
}
entityID := tokenLookupResponse.Data["entity_id"].(string)
_, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{
"metadata": map[string]string{
"ssh_username": testUserName,
},
})
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"default_user": "{{identity.entity.metadata.ssh_username}}",
// disable user templating but not allowed_user_template and the request should fail
"default_user_template": false,
"allowed_users": "{{identity.entity.metadata.ssh_username}}",
"allowed_users_template": true,
})
if err != nil {
t.Fatal(err)
}

// sign SSH key as userpass user
client.SetToken(userpassToken)
_, err = client.Logical().Write("ssh/sign/my-role", map[string]interface{}{
"public_key": testCAPublicKey,
})
if err == nil {
t.Errorf("signing request should fail when default_user is not in the allowed_users list, because allowed_users_template is true and default_user_template is not")
}

expectedErrStr := ":TKTKT"
if !strings.Contains(err.Error(), expectedErrStr) {
t.Errorf("expected error to include %q but it was: %q", expectedErrStr, err.Error())
}
}

func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateFalse(t *testing.T) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup()
client := cluster.Cores[0].Client

// set metadata "ssh_username" to userpass username
tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{
"token": userpassToken,
})
if err != nil {
t.Fatal(err)
}
entityID := tokenLookupResponse.Data["entity_id"].(string)
_, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{
"metadata": map[string]string{
"ssh_username": testUserName,
},
})
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"default_user": "{{identity.entity.metadata.ssh_username}}",
"default_user_template": false,
"allowed_users": "{{identity.entity.metadata.ssh_username}}",
"allowed_users_template": false,
})
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,
})
if err != nil {
t.Fatal(err)
}

// check for the expected valid principals of certificate
signedKey := signResponse.Data["signed_key"].(string)
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
parsedKey, err := ssh.ParsePublicKey(key)
if err != nil {
t.Fatal(err)
}
actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals
if len(actualPrincipals) < 1 {
t.Fatal(
fmt.Sprintf("No ValidPrincipals returned: should have been %v",
[]string{"{{identity.entity.metadata.ssh_username}}"}),
)
}
if len(actualPrincipals) > 1 {
t.Error(
fmt.Sprintf("incorrect number ValidPrincipals, expected only 1: %v should be %v",
actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}),
)
}
if actualPrincipals[0] != "{{identity.entity.metadata.ssh_username}}" {
t.Fatal(
fmt.Sprintf("incorrect ValidPrincipals: %v should be %v",
actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}),
)
}
}

func newTestingFactory(t *testing.T) func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
defaultLeaseTTLVal := 2 * time.Minute
Expand Down Expand Up @@ -1893,6 +2034,66 @@ func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster,
return cluster, userpassToken
}

// TKTK
func testDefaultUserTemplate(t *testing.T, testDefaultUserTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string,
) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup()
client := cluster.Cores[0].Client

// set metadata "ssh_username" to userpass username
tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{
"token": userpassToken,
})
if err != nil {
t.Fatal(err)
}
entityID := tokenLookupResponse.Data["entity_id"].(string)
_, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{
"metadata": testEntityMetadata,
})
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"default_user": testDefaultUserTemplate,
"default_user_template": true,
"allowed_users": testDefaultUserTemplate,
"allowed_users_template": true,
})
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,
})
if err != nil {
t.Fatal(err)
}

// check for the expected valid principals of certificate
signedKey := signResponse.Data["signed_key"].(string)
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
parsedKey, err := ssh.ParsePublicKey(key)
if err != nil {
t.Fatal(err)
}
actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals
if actualPrincipals[0] != expectedValidPrincipal {
t.Fatal(
fmt.Sprintf("incorrect ValidPrincipals: %v should be %v",
actualPrincipals, []string{expectedValidPrincipal}),
)
}
}

func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string,
) {
Expand Down
47 changes: 30 additions & 17 deletions builtin/logical/ssh/path_issue_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
return logical.ErrorResponse(err.Error()), nil
}
} else {
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, role.DefaultUser, role.AllowedUsers, strutil.StrListContains)
defaultPrincipal := role.DefaultUser
if role.DefaultUserTemplate {
defaultPrincipal, err = b.renderPrincipal(role.DefaultUser, req)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
}
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, strutil.StrListContains)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
Expand Down Expand Up @@ -136,6 +143,23 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
return response, nil
}

func (b *backend) renderPrincipal(principal string, req *logical.Request) (string, error) {
// Look for templating markers {{ .* }}
matched := containsTemplateRegex.MatchString(principal)
if matched {
if req.EntityID != "" {
// Retrieve principal based on template + entityID from request.
renderedPrincipal, err := framework.PopulateIdentityTemplate(principal, req.EntityID, b.System())
if err != nil {
return "", fmt.Errorf("template '%s' could not be rendered -> %s", principal, err)
}
return renderedPrincipal, nil
}
}
// Static principal
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) {
validPrincipals := ""
validPrincipalsRaw, ok := data.GetOk("valid_principals")
Expand All @@ -150,23 +174,12 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic
var allowedPrincipals []string
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
if role.AllowedUsersTemplate {
// Look for templating markers {{ .* }}
matched := containsTemplateRegex.MatchString(principal)
if matched {
if req.EntityID != "" {
// Retrieve principal based on template + entityID from request.
templatePrincipal, err := framework.PopulateIdentityTemplate(principal, req.EntityID, b.System())
if err == nil {
// Template returned a principal
allowedPrincipals = append(allowedPrincipals, templatePrincipal)
} else {
return nil, fmt.Errorf("template '%s' could not be rendered -> %s", principal, err)
}
}
} else {
// Static principal or err template
allowedPrincipals = append(allowedPrincipals, principal)
rendered, err := b.renderPrincipal(principal, req)
if err != nil {
return nil, fmt.Errorf("template '%s' could not be rendered -> %s", principal, err)
}
// Template returned a principal
allowedPrincipals = append(allowedPrincipals, rendered)
} else {
// Static principal
allowedPrincipals = append(allowedPrincipals, principal)
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 @@ -40,6 +40,7 @@ type sshRole struct {
KeyBits int `mapstructure:"key_bits" json:"key_bits"`
AdminUser string `mapstructure:"admin_user" json:"admin_user"`
DefaultUser string `mapstructure:"default_user" json:"default_user"`
DefaultUserTemplate bool `mapstructure:"default_user_template" json:"default_user_template"`
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
ExcludeCIDRList string `mapstructure:"exclude_cidr_list" json:"exclude_cidr_list"`
Port int `mapstructure:"port" json:"port"`
Expand Down Expand Up @@ -122,6 +123,15 @@ func pathRoles(b *backend) *framework.Path {
Name: "Default Username",
},
},
"default_user_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
If set, Default user can be specified using identity template policies.
Non-templated users are also permitted.
`,
Default: false,
},
"cidr_list": {
Type: framework.TypeString,
Description: `
Expand Down Expand Up @@ -558,6 +568,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser, signer string, data *f
AllowedUsersTemplate: data.Get("allowed_users_template").(bool),
AllowedDomains: data.Get("allowed_domains").(string),
DefaultUser: defaultUser,
DefaultUserTemplate: data.Get("default_user_template").(bool),
AllowBareDomains: data.Get("allow_bare_domains").(bool),
AllowSubdomains: data.Get("allow_subdomains").(bool),
AllowUserKeyIDs: data.Get("allow_user_key_ids").(bool),
Expand Down Expand Up @@ -740,6 +751,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"allowed_users_template": role.AllowedUsersTemplate,
"allowed_domains": role.AllowedDomains,
"default_user": role.DefaultUser,
"default_user_template": role.DefaultUserTemplate,
"ttl": int64(ttl.Seconds()),
"max_ttl": int64(maxTTL.Seconds()),
"allowed_critical_options": role.AllowedCriticalOptions,
Expand Down
3 changes: 3 additions & 0 deletions changelog/16351.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Allow the use of Identity templates in the `default_user` field
```

0 comments on commit 3aa20e0

Please sign in to comment.