From 6f466a59d4b8d2d727cfeab828272e789b0c6660 Mon Sep 17 00:00:00 2001 From: Peter Verraedt Date: Tue, 23 Aug 2022 20:03:53 +0200 Subject: [PATCH] auth/cert: Add metadata to identity-alias (#14751) * auth/cert: Add metadata to identity-alias Add the possibility to include certificate metadata in the created logical.Alias (the identity alias), in addition to the metadata added to logical.Auth. This is analogous to the behaviour of the ldap and approle auth providers. This possibility can be configured by the config endpoint of the auth method mount and is disabled by default. We added the read operation on this config endpoint as well. Fixes: #14418 Signed-off-by: Peter Verraedt * Add changelog for #14751 Signed-off-by: Peter Verraedt * Test the usage of cert metadata in ACL policies Signed-off-by: Peter Verraedt Signed-off-by: Peter Verraedt Signed-off-by: Peter Verraedt --- builtin/credential/cert/backend_test.go | 224 +++++++++++++++++++++++- builtin/credential/cert/path_config.go | 29 ++- builtin/credential/cert/path_login.go | 10 ++ changelog/14751.txt | 4 + 4 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 changelog/14751.txt diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index db400dab780a0..3c6948a3e866b 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -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 @@ -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), }, }) } @@ -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, "") } @@ -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", @@ -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"}) diff --git a/builtin/credential/cert/path_config.go b/builtin/credential/cert/path_config.go index e73aeb3ec0963..9cc17f3a6aafe 100644 --- a/builtin/credential/cert/path_config.go +++ b/builtin/credential/cert/path_config.go @@ -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 @@ -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") @@ -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"` } diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index f45e39f2e0692..c78188b7cc9b6 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -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 @@ -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{ diff --git a/changelog/14751.txt b/changelog/14751.txt new file mode 100644 index 0000000000000..17cbfa02bdcbc --- /dev/null +++ b/changelog/14751.txt @@ -0,0 +1,4 @@ + +```release-note:improvement +auth/cert: Add metadata to identity-alias +```