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

auth/cert: Add metadata to identity-alias #14751

Merged
merged 3 commits into from Aug 23, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
224 changes: 221 additions & 3 deletions builtin/credential/cert/backend_test.go
Expand Up @@ -458,6 +458,166 @@ func TestBackend_PermittedDNSDomainsIntermediateCA(t *testing.T) {
}
}

func TestBackend_MetadataBasedACLPolicy(t *testing.T) {
// Start cluster with cert auth method enabled
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: log.NewNullLogger(),
CredentialBackends: map[string]logical.Factory{
"cert": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client

var err error

// Enable the cert auth method
err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{
Type: "cert",
})
if err != nil {
t.Fatal(err)
}

// Enable metadata in aliases
_, err = client.Logical().Write("auth/cert/config", map[string]interface{}{
"enable_identity_alias_metadata": true,
})
if err != nil {
t.Fatal(err)
}

// Retrieve its accessor id
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}

var accessor string

for _, auth := range auths {
if auth.Type == "cert" {
accessor = auth.Accessor
}
}

if accessor == "" {
t.Fatal("failed to find cert auth accessor")
}

// Write ACL policy
err = client.Sys().PutPolicy("metadata-based", fmt.Sprintf(`
path "kv/cn/{{identity.entity.aliases.%s.metadata.common_name}}" {
capabilities = ["read"]
}
path "kv/ext/{{identity.entity.aliases.%s.metadata.2-1-1-1}}" {
capabilities = ["read"]
}
`, accessor, accessor))
if err != nil {
t.Fatalf("err: %v", err)
}

ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}

// Set the trusted certificate in the backend
_, err = client.Logical().Write("auth/cert/certs/test", map[string]interface{}{
"display_name": "test",
"policies": "metadata-based",
"certificate": string(ca),
"allowed_metadata_extensions": "2.1.1.1,1.2.3.45",
})
if err != nil {
t.Fatal(err)
}

// This function is a copy-paste from the NewTestCluster, with the
// modification to reconfigure the TLS on the api client with a
// specific client certificate.
getAPIClient := func(port int, tlsConfig *tls.Config) *api.Client {
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = tlsConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
client := &http.Client{
Transport: transport,
CheckRedirect: func(*http.Request, []*http.Request) error {
// This can of course be overridden per-test by using its own client
return fmt.Errorf("redirects not allowed in these tests")
},
}
config := api.DefaultConfig()
if config.Error != nil {
t.Fatal(config.Error)
}
config.Address = fmt.Sprintf("https://127.0.0.1:%d", port)
config.HttpClient = client

// Set the client certificates
config.ConfigureTLS(&api.TLSConfig{
CACertBytes: cluster.CACertPEM,
ClientCert: "test-fixtures/root/rootcawextcert.pem",
ClientKey: "test-fixtures/root/rootcawextkey.pem",
})

apiClient, err := api.NewClient(config)
if err != nil {
t.Fatal(err)
}
return apiClient
}

// Create a new api client with the desired TLS configuration
newClient := getAPIClient(cores[0].Listeners[0].Address.Port, cores[0].TLSConfig)

var secret *api.Secret

secret, err = newClient.Logical().Write("auth/cert/login", map[string]interface{}{
"name": "test",
})
if err != nil {
t.Fatal(err)
}
if secret.Auth == nil || secret.Auth.ClientToken == "" {
t.Fatalf("expected a successful authentication")
}

// Check paths guarded by ACL policy
newClient.SetToken(secret.Auth.ClientToken)

_, err = newClient.Logical().Read("kv/cn/example.com")
if err != nil {
t.Fatal(err)
}

_, err = newClient.Logical().Read("kv/cn/not.example.com")
if err == nil {
t.Fatal("expected access denied")
}

_, err = newClient.Logical().Read("kv/ext/A UTF8String Extension")
if err != nil {
t.Fatal(err)
}

_, err = newClient.Logical().Read("kv/ext/bar")
if err == nil {
t.Fatal("expected access denied")
}
}

func TestBackend_NonCAExpiry(t *testing.T) {
var resp *logical.Response
var err error
Expand Down Expand Up @@ -1107,10 +1267,17 @@ func TestBackend_ext_singleCert(t *testing.T) {
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: false}, connState),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, false),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, false),
testAccStepSetConfig(t, config{EnableIdentityAliasMetadata: true}, connState),
testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: true}, connState),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, true),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, true),
},
})
}
Expand Down Expand Up @@ -1515,6 +1682,42 @@ func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicalte
}
}

func testAccStepSetConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
ConnState: &connState,
Data: map[string]interface{}{
"enable_identity_alias_metadata": conf.EnableIdentityAliasMetadata,
},
}
}

func testAccStepReadConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config",
ConnState: &connState,
Check: func(resp *logical.Response) error {
value, ok := resp.Data["enable_identity_alias_metadata"]
if !ok {
t.Fatalf("enable_identity_alias_metadata not found in response")
}

b, ok := value.(bool)
if !ok {
t.Fatalf("bad: expected enable_identity_alias_metadata to be a bool")
}

if b != conf.EnableIdentityAliasMetadata {
t.Fatalf("bad: expected enable_identity_alias_metadata to be %t, got %t", conf.EnableIdentityAliasMetadata, b)
}

return nil
},
}
}

func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return testAccStepLoginWithName(t, connState, "")
}
Expand Down Expand Up @@ -1560,7 +1763,7 @@ func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) l
}
}

func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, certName string, metadata map[string]string) logicaltest.TestStep {
func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, certName string, metadata map[string]string, expectAliasMetadata bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Expand All @@ -1583,6 +1786,21 @@ func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, c
if value != expected {
t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value)
}

if expectAliasMetadata {
value, ok = resp.Auth.Alias.Metadata[key]
if !ok {
t.Fatalf("missing alias metadata key: %s", key)
}

if value != expected {
t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value)
}
} else {
if len(resp.Auth.Alias.Metadata) > 0 {
t.Fatal("found alias metadata keys, but should not have any")
}
}
}

fn := logicaltest.TestCheckAuth([]string{"default", "foo"})
Expand Down
29 changes: 27 additions & 2 deletions builtin/credential/cert/path_config.go
Expand Up @@ -17,19 +17,27 @@ func pathConfig(b *backend) *framework.Path {
Default: false,
Description: `If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false.`,
},
"enable_identity_alias_metadata": {
Type: framework.TypeBool,
Default: false,
Description: `If set, metadata of the certificate including the metadata corresponding to allowed_metadata_extensions will be stored in the alias. Defaults to false.`,
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathConfigWrite,
logical.ReadOperation: b.pathConfigRead,
},
}
}

func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
disableBinding := data.Get("disable_binding").(bool)
enableIdentityAliasMetadata := data.Get("enable_identity_alias_metadata").(bool)

entry, err := logical.StorageEntryJSON("config", config{
DisableBinding: disableBinding,
DisableBinding: disableBinding,
EnableIdentityAliasMetadata: enableIdentityAliasMetadata,
})
if err != nil {
return nil, err
Expand All @@ -41,6 +49,22 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat
return nil, nil
}

func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}

data := map[string]interface{}{
"disable_binding": cfg.DisableBinding,
"enable_identity_alias_metadata": cfg.EnableIdentityAliasMetadata,
}

return &logical.Response{
Data: data,
}, nil
}

// Config returns the configuration for this backend.
func (b *backend) Config(ctx context.Context, s logical.Storage) (*config, error) {
entry, err := s.Get(ctx, "config")
Expand All @@ -59,5 +83,6 @@ func (b *backend) Config(ctx context.Context, s logical.Storage) (*config, error
}

type config struct {
DisableBinding bool `json:"disable_binding"`
DisableBinding bool `json:"disable_binding"`
EnableIdentityAliasMetadata bool `json:"enable_identity_alias_metadata"`
}
10 changes: 10 additions & 0 deletions builtin/credential/cert/path_login.go
Expand Up @@ -77,6 +77,11 @@ func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Requ
}

func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}

var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(ctx, req, data); err != nil {
return nil, err
Expand Down Expand Up @@ -132,6 +137,11 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *fra
Name: clientCerts[0].Subject.CommonName,
},
}

if config.EnableIdentityAliasMetadata {
auth.Alias.Metadata = metadata
}

matched.Entry.PopulateTokenAuth(auth)

return &logical.Response{
Expand Down
4 changes: 4 additions & 0 deletions changelog/14751.txt
@@ -0,0 +1,4 @@

```release-note:improvement
auth/cert: Add metadata to identity-alias
```