From 9d93afeb475f0a44fca0eac65bac3b31e245e9ba Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Tue, 20 Oct 2026 14:39:35 +0200 Subject: [PATCH 1/2] credential-provider: reload the namespace policy when its file changes The Kubernetes secrets provider read its namespace policy once at startup, so every policy edit needed a provider restart. It now rereads the file when its stat changes, which a ConfigMap volume update always causes. A file that fails to load is logged and leaves the last good policy in force, and /statusz shows the error. A malformed file still fails startup. --- .../kubernetes-secrets/kubeprovider.go | 4 + .../kubernetes-secrets/kubeprovider_test.go | 85 ++++++++++++++++++ .../kubernetes-secrets/nsauthz.go | 89 +++++++++++++++++-- .../kubernetes-secrets/statusz.go | 13 ++- docs/egress-credential-injection.md | 10 +-- internal/e2e/credprovider.go | 5 +- .../namespace-policy.yaml | 5 +- 7 files changed, 188 insertions(+), 23 deletions(-) diff --git a/cmd/credential-provider/kubernetes-secrets/kubeprovider.go b/cmd/credential-provider/kubernetes-secrets/kubeprovider.go index d6e84057bc..3f47e00c14 100644 --- a/cmd/credential-provider/kubernetes-secrets/kubeprovider.go +++ b/cmd/credential-provider/kubernetes-secrets/kubeprovider.go @@ -133,6 +133,10 @@ func NewServer(client kubernetes.Interface, nsAuth *NamespaceAuthorizer) *Server // when authorization is disabled. func (s *Server) Grants() map[string][]string { return s.nsAuth.Grants() } +// PolicyError reports why the policy file failed to load while an older +// policy stays in force, for /statusz. +func (s *Server) PolicyError() error { return s.nsAuth.LoadError() } + // FetchSecret resolves one ate-secret:// URI to its Secret value. func (s *Server) FetchSecret(ctx context.Context, req *credproviderpb.FetchSecretRequest) (*credproviderpb.FetchSecretResponse, error) { ref, err := ParseURI(req.GetUri()) diff --git a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go index 57a67f8d09..8e1d712b96 100644 --- a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go +++ b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go @@ -456,6 +456,91 @@ func TestLoadNamespaceAuthorizerRejectsUnknownFields(t *testing.T) { } } +// The provider rereads its policy when the file changes, and keeps the last +// good policy while the file fails to load. +func TestLoadNamespaceAuthorizerReloads(t *testing.T) { + dir := t.TempDir() + path := dir + "/policy.yaml" + // Each step replaces the file the way a ConfigMap volume update does: a + // new file is renamed over the old one. + replace := func(t *testing.T, content string) { + t.Helper() + tmp := dir + "/next.yaml" + if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil { + t.Fatalf("write policy: %v", err) + } + if err := os.Rename(tmp, path); err != nil { + t.Fatalf("replace policy: %v", err) + } + } + replace(t, "policies:\n- atespace: team-a\n allowedNamespaces: [ns1]\n") + authz, err := LoadNamespaceAuthorizer(path) + if err != nil { + t.Fatalf("LoadNamespaceAuthorizer: %v", err) + } + + steps := []struct { + name string + // content replaces the file; empty removes it. + content string + wantAllowed []string + wantRefused []string + wantErr bool + }{ + { + name: "grant moves to another namespace", + content: "policies:\n- atespace: team-a\n allowedNamespaces: [ns2]\n", + wantAllowed: []string{"ns2"}, + wantRefused: []string{"ns1"}, + }, + { + name: "malformed file keeps the previous policy", + content: "policies:\n- atespace: team-a\n allowedNamespace: [ns3]\n", + wantAllowed: []string{"ns2"}, + wantRefused: []string{"ns3"}, + wantErr: true, + }, + { + name: "fixed file loads", + content: "policies:\n- atespace: team-a\n allowedNamespaces: [ns3]\n", + wantAllowed: []string{"ns3"}, + wantRefused: []string{"ns2"}, + }, + { + name: "missing file keeps the previous policy", + wantAllowed: []string{"ns3"}, + wantErr: true, + }, + { + name: "file comes back", + content: "policies: []\n", + wantRefused: []string{"ns3"}, + }, + } + for _, step := range steps { + if step.content == "" { + if err := os.Remove(path); err != nil { + t.Fatalf("%s: remove policy: %v", step.name, err) + } + } else { + replace(t, step.content) + } + for _, ns := range step.wantAllowed { + if !authz.Allowed("team-a", ns) || !authz.AllowedSecret("team-a", ns, nil) { + t.Errorf("%s: team-a/%s refused, want allowed", step.name, ns) + } + } + for _, ns := range step.wantRefused { + if authz.Allowed("team-a", ns) || authz.AllowedSecret("team-a", ns, nil) { + t.Errorf("%s: team-a/%s allowed, want refused", step.name, ns) + } + } + if err := authz.LoadError(); (err != nil) != step.wantErr { + t.Errorf("%s: LoadError() = %v, want error %v", step.name, err, step.wantErr) + } + } +} + // Each policy is its own grant and they are OR-ed. Merging them into one would // let a narrow policy take away what a broad one granted, and would AND the // selectors of separate policies together. diff --git a/cmd/credential-provider/kubernetes-secrets/nsauthz.go b/cmd/credential-provider/kubernetes-secrets/nsauthz.go index 2b7cee3115..3a3e1a92e6 100644 --- a/cmd/credential-provider/kubernetes-secrets/nsauthz.go +++ b/cmd/credential-provider/kubernetes-secrets/nsauthz.go @@ -16,8 +16,10 @@ package main import ( "fmt" + "log/slog" "os" "sort" + "sync" "k8s.io/apimachinery/pkg/util/validation" @@ -69,8 +71,20 @@ type grant struct { // NamespaceAuthorizer decides whether an atespace may resolve secrets in a given // Kubernetes namespace. It is default-deny: an atespace absent from the mapping -// can resolve nothing. +// can resolve nothing. One loaded from a file rereads the file when it changes. type NamespaceAuthorizer struct { + // path is the policy file; empty when the authorizer was built from an + // already parsed policy and never reloads. + path string + + mu sync.Mutex + // fi is the stat of path taken before its last parse, successful or not, + // so a file that fails to parse is reported once rather than per request. + // Nil when path could not be stat'ed, so the next call tries again. + fi os.FileInfo + // loadErr is why the last attempt to load path failed; nil when allowed + // came from the file as it is now. + loadErr error // allowed maps atespace -> namespace -> the grants that apply there. Each // policy contributes its own grant and they are OR-ed: a Secret is admitted // when any one of them admits it. Within a grant the criteria are AND-ed, @@ -79,8 +93,24 @@ type NamespaceAuthorizer struct { } // LoadNamespaceAuthorizer reads the YAML policy file at path and builds an -// authorizer, so a malformed file fails startup rather than the first request. +// authorizer that rereads the file whenever it changes. A malformed file fails +// here, at startup; once running, a file that fails to load leaves the last +// good policy in force. func LoadNamespaceAuthorizer(path string) (*NamespaceAuthorizer, error) { + fi, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("reading namespace policy file %q: %w", path, err) + } + a, err := readNamespacePolicy(path) + if err != nil { + return nil, err + } + a.path, a.fi = path, fi + return a, nil +} + +// readNamespacePolicy parses the YAML policy file at path. +func readNamespacePolicy(path string) (*NamespaceAuthorizer, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading namespace policy file %q: %w", path, err) @@ -133,13 +163,57 @@ func newNamespaceAuthorizer(file namespacePolicyFile) (*NamespaceAuthorizer, err return &NamespaceAuthorizer{allowed: allowed}, nil } +// policy returns the grants in force, first rereading the policy file if its +// stat no longer matches the last one parsed. A ConfigMap volume swaps in a new +// file on update, so the stat changes even when the size does not. +func (a *NamespaceAuthorizer) policy() map[string]map[string][]grant { + a.mu.Lock() + defer a.mu.Unlock() + if a.path == "" { + return a.allowed + } + fi, err := os.Stat(a.path) + if err == nil && a.fi != nil && os.SameFile(a.fi, fi) && fi.ModTime().Equal(a.fi.ModTime()) && fi.Size() == a.fi.Size() { + return a.allowed + } + a.fi = nil + var next *NamespaceAuthorizer + if err == nil { + a.fi = fi + next, err = readNamespacePolicy(a.path) + } + if err != nil { + if a.loadErr == nil || a.loadErr.Error() != err.Error() { + slog.Error("namespace policy reload failed, keeping the previous policy", slog.String("file", a.path), slog.Any("err", err)) + } + a.loadErr = err + return a.allowed + } + a.allowed, a.loadErr = next.allowed, nil + slog.Info("reloaded namespace authorization policy", slog.String("file", a.path)) + return a.allowed +} + +// LoadError reports why the policy file failed to load, or nil when the +// policy in force is the file's current content. +func (a *NamespaceAuthorizer) LoadError() error { + if a == nil { + return nil + } + a.policy() + a.mu.Lock() + defer a.mu.Unlock() + return a.loadErr +} + // Grants returns the loaded policy as atespace → sorted namespaces func (a *NamespaceAuthorizer) Grants() map[string][]string { if a == nil { return nil } - out := make(map[string][]string, len(a.allowed)) - for atespace, namespaces := range a.allowed { + allowed := a.policy() + out := make(map[string][]string, len(allowed)) + for atespace, namespaces := range allowed { list := make([]string, 0, len(namespaces)) for ns := range namespaces { list = append(list, ns) @@ -154,7 +228,7 @@ func (a *NamespaceAuthorizer) Grants() map[string][]string { // deny: an atespace absent from the mapping, or a namespace not in its list, is // refused. func (a *NamespaceAuthorizer) Allowed(atespace, namespace string) bool { - set, ok := a.allowed[atespace] + set, ok := a.policy()[atespace] if !ok { return false } @@ -164,9 +238,10 @@ func (a *NamespaceAuthorizer) Allowed(atespace, namespace string) bool { // AllowedSecret reports whether atespace may resolve a Secret with these // labels in namespace. A grant without a label selector admits every Secret, // including one that does not exist: pass nil labels to ask that question -// before reading anything from Kubernetes. +// before reading anything from Kubernetes. The namespace grant is checked again +// here, so a request that straddles a reload is settled by the newer policy. func (a *NamespaceAuthorizer) AllowedSecret(atespace, namespace string, labels map[string]string) bool { - for _, g := range a.allowed[atespace][namespace] { + for _, g := range a.policy()[atespace][namespace] { if len(g.labels) == 0 { return true } diff --git a/cmd/credential-provider/kubernetes-secrets/statusz.go b/cmd/credential-provider/kubernetes-secrets/statusz.go index df586ff021..61c39d5ed5 100644 --- a/cmd/credential-provider/kubernetes-secrets/statusz.go +++ b/cmd/credential-provider/kubernetes-secrets/statusz.go @@ -15,9 +15,9 @@ // The provider's /statusz page, mirroring the atenet router's: one handler // serving an HTML dashboard by default and JSON under ?format=json (or // Accept: application/json). It shows the provider's configuration as the -// running process holds it — notably the authorization policy loaded at -// startup, which the ConfigMap on disk may have since drifted from. Names -// and configuration only, never secret values. +// running process holds it — notably the authorization policy in force, which +// stays at the last version that loaded while the file on disk fails to parse. +// Names and configuration only, never secret values. package main import ( @@ -41,6 +41,7 @@ type statusContext struct { Flags map[string]string `json:"flags"` InjectorSAN string `json:"injector_san"` PolicyGrants map[string][]string `json:"policy_grants"` + PolicyError string `json:"policy_error,omitempty"` } // newStatuszHandler serves /statusz over the provider's state. @@ -57,6 +58,9 @@ func newStatuszHandler(srv *Server) http.HandlerFunc { InjectorSAN: *injectorIdentity, PolicyGrants: srv.Grants(), } + if err := srv.PolicyError(); err != nil { + data.PolicyError = err.Error() + } if strings.Contains(req.Header.Get("Accept"), "application/json") || req.URL.Query().Get("format") == "json" { w.Header().Set("Content-Type", "application/json") @@ -88,7 +92,8 @@ provider: {{.ProviderName}}
required caller SAN: {{.InjectorSAN}}
args: {{.Args}}

-

Namespace policy (as loaded at startup)

+

Namespace policy

+{{if .PolicyError}}

policy file failed to load, previous policy in force: {{.PolicyError}}

{{end}} {{if .PolicyGrants}} diff --git a/docs/egress-credential-injection.md b/docs/egress-credential-injection.md index d402b017a7..6a7602ac37 100644 --- a/docs/egress-credential-injection.md +++ b/docs/egress-credential-injection.md @@ -244,12 +244,10 @@ kubectl -n ate-system edit configmap k8s-credential-provider-namespace-policy - ns1 ``` -The provider loads the policy **once at startup** and does not yet reload it, -so after editing, restart the provider: - -```bash -kubectl -n ate-system rollout restart deployment/k8s-credential-provider -``` +The provider rereads the policy when the kubelet updates the mounted ConfigMap, +which can take a minute or so; no restart is needed. If the edited policy fails +to parse, the provider logs the error and keeps enforcing the previous one; +`/statusz` on the provider shows the error and the grants in force. For a first smoke test, grant your actor's atespace `ns1` as above and apply a sample secret that the URI in the sample policy resolves to: diff --git a/internal/e2e/credprovider.go b/internal/e2e/credprovider.go index bd18c9d819..c575ecf60b 100644 --- a/internal/e2e/credprovider.go +++ b/internal/e2e/credprovider.go @@ -49,8 +49,9 @@ const ( // ConfigureCredentialProvider points the install's k8s-credential-provider at // the credinject fixture: the Secret behind CredentialInjectionURI and the // authorization policy that lets the probe atespaces resolve it. The provider -// Deployment is restarted after the policy ConfigMap is applied because it -// reads the policy once at startup. When the test passes the fixture is +// Deployment is restarted after the policy ConfigMap is applied, which is +// quicker than waiting for the kubelet to update the mounted policy the +// provider rereads. When the test passes the fixture is // removed, the shipped policy put back, and the provider restarted again so // it drops the fixture's grants. // diff --git a/manifests/egress-credential-injection/namespace-policy.yaml b/manifests/egress-credential-injection/namespace-policy.yaml index e90b9c6c32..94a55c6474 100644 --- a/manifests/egress-credential-injection/namespace-policy.yaml +++ b/manifests/egress-credential-injection/namespace-policy.yaml @@ -16,10 +16,7 @@ # atespace may only resolve Secrets whose URI namespace is listed for it here. # Enforcement is default-deny — an atespace absent from this file resolves # nothing. The install creates this ConfigMap only when it is absent, granting -# nothing; the provider needs it to start. -# -# TODO: k8s-credential-provider loads this once at startup, so editing this ConfigMap -# requires restarting the k8s-credential-provider Deployment. Make it reload dynamically. +# nothing; the provider needs it to start, and rereads it when it changes. apiVersion: v1 kind: ConfigMap metadata: From 99fb9956c5de85ad53875bd353f650dc571f1e74 Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Tue, 20 Oct 2026 14:47:07 +0200 Subject: [PATCH 2/2] credential-provider: load the namespace policy at startup and on change through one path LoadNamespaceAuthorizer now runs the same stat-and-parse as a reload, and policy returns the load error with the grants. The reload test swaps a ..data symlink as the kubelet does, so it fails if the stat stops following the link. --- .../kubernetes-secrets/kubeprovider_test.go | 32 ++++++-- .../kubernetes-secrets/main.go | 1 - .../kubernetes-secrets/nsauthz.go | 75 ++++++++++--------- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go index 8e1d712b96..35a87037d1 100644 --- a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go +++ b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go @@ -16,7 +16,9 @@ package main import ( "context" + "fmt" "os" + "path/filepath" "strings" "testing" @@ -460,17 +462,31 @@ func TestLoadNamespaceAuthorizerRejectsUnknownFields(t *testing.T) { // good policy while the file fails to load. func TestLoadNamespaceAuthorizerReloads(t *testing.T) { dir := t.TempDir() - path := dir + "/policy.yaml" - // Each step replaces the file the way a ConfigMap volume update does: a - // new file is renamed over the old one. + path := filepath.Join(dir, "policy.yaml") + dataLink := filepath.Join(dir, "..data") + // path resolves through the ..data symlink, and each step swaps that + // symlink to a new directory, the way the kubelet updates a ConfigMap + // volume. + if err := os.Symlink(filepath.Join("..data", "policy.yaml"), path); err != nil { + t.Fatalf("link policy: %v", err) + } + version := 0 replace := func(t *testing.T, content string) { t.Helper() - tmp := dir + "/next.yaml" - if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil { + version++ + target := fmt.Sprintf("..v%d", version) + if err := os.Mkdir(filepath.Join(dir, target), 0o700); err != nil { + t.Fatalf("create policy dir: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, target, "policy.yaml"), []byte(content), 0o600); err != nil { t.Fatalf("write policy: %v", err) } - if err := os.Rename(tmp, path); err != nil { - t.Fatalf("replace policy: %v", err) + tmp := filepath.Join(dir, "..data_tmp") + if err := os.Symlink(target, tmp); err != nil { + t.Fatalf("link policy dir: %v", err) + } + if err := os.Rename(tmp, dataLink); err != nil { + t.Fatalf("swap policy dir: %v", err) } } replace(t, "policies:\n- atespace: team-a\n allowedNamespaces: [ns1]\n") @@ -519,7 +535,7 @@ func TestLoadNamespaceAuthorizerReloads(t *testing.T) { } for _, step := range steps { if step.content == "" { - if err := os.Remove(path); err != nil { + if err := os.Remove(dataLink); err != nil { t.Fatalf("%s: remove policy: %v", step.name, err) } } else { diff --git a/cmd/credential-provider/kubernetes-secrets/main.go b/cmd/credential-provider/kubernetes-secrets/main.go index 544fff4ceb..967d3bc140 100644 --- a/cmd/credential-provider/kubernetes-secrets/main.go +++ b/cmd/credential-provider/kubernetes-secrets/main.go @@ -110,7 +110,6 @@ func run(ctx context.Context) error { if err != nil { return fmt.Errorf("namespace policy: %w", err) } - slog.InfoContext(ctx, "loaded namespace authorization policy", slog.String("file", *nsPolicyFile)) creds, err := buildServerCreds(ctx) if err != nil { diff --git a/cmd/credential-provider/kubernetes-secrets/nsauthz.go b/cmd/credential-provider/kubernetes-secrets/nsauthz.go index 3a3e1a92e6..cbbc51d4a1 100644 --- a/cmd/credential-provider/kubernetes-secrets/nsauthz.go +++ b/cmd/credential-provider/kubernetes-secrets/nsauthz.go @@ -79,11 +79,11 @@ type NamespaceAuthorizer struct { mu sync.Mutex // fi is the stat of path taken before its last parse, successful or not, - // so a file that fails to parse is reported once rather than per request. - // Nil when path could not be stat'ed, so the next call tries again. + // so a file that fails to parse is read and logged once rather than per + // request. Nil when path could not be stat'ed, so the next call retries. fi os.FileInfo - // loadErr is why the last attempt to load path failed; nil when allowed - // came from the file as it is now. + // loadErr is why the file as it is now failed to load; allowed then holds + // the last policy that loaded. loadErr error // allowed maps atespace -> namespace -> the grants that apply there. Each // policy contributes its own grant and they are OR-ed: a Secret is admitted @@ -97,15 +97,10 @@ type NamespaceAuthorizer struct { // here, at startup; once running, a file that fails to load leaves the last // good policy in force. func LoadNamespaceAuthorizer(path string) (*NamespaceAuthorizer, error) { - fi, err := os.Stat(path) - if err != nil { - return nil, fmt.Errorf("reading namespace policy file %q: %w", path, err) - } - a, err := readNamespacePolicy(path) - if err != nil { + a := &NamespaceAuthorizer{path: path} + if _, err := a.policy(); err != nil { return nil, err } - a.path, a.fi = path, fi return a, nil } @@ -164,34 +159,42 @@ func newNamespaceAuthorizer(file namespacePolicyFile) (*NamespaceAuthorizer, err } // policy returns the grants in force, first rereading the policy file if its -// stat no longer matches the last one parsed. A ConfigMap volume swaps in a new -// file on update, so the stat changes even when the size does not. -func (a *NamespaceAuthorizer) policy() map[string]map[string][]grant { +// stat no longer matches the last one parsed. The kubelet updates a ConfigMap +// volume by swapping in a new file, so the stat changes even when the size +// does not. The error is why the file as it is now failed to load. +func (a *NamespaceAuthorizer) policy() (map[string]map[string][]grant, error) { a.mu.Lock() defer a.mu.Unlock() if a.path == "" { - return a.allowed + return a.allowed, nil } fi, err := os.Stat(a.path) - if err == nil && a.fi != nil && os.SameFile(a.fi, fi) && fi.ModTime().Equal(a.fi.ModTime()) && fi.Size() == a.fi.Size() { - return a.allowed + if err != nil { + a.fi = nil + return a.allowed, a.loadFailed(fmt.Errorf("reading namespace policy file %q: %w", a.path, err)) } - a.fi = nil - var next *NamespaceAuthorizer - if err == nil { - a.fi = fi - next, err = readNamespacePolicy(a.path) + if a.fi != nil && os.SameFile(a.fi, fi) && fi.ModTime().Equal(a.fi.ModTime()) && fi.Size() == a.fi.Size() { + return a.allowed, a.loadErr } + a.fi = fi + next, err := readNamespacePolicy(a.path) if err != nil { - if a.loadErr == nil || a.loadErr.Error() != err.Error() { - slog.Error("namespace policy reload failed, keeping the previous policy", slog.String("file", a.path), slog.Any("err", err)) - } - a.loadErr = err - return a.allowed + return a.allowed, a.loadFailed(err) } a.allowed, a.loadErr = next.allowed, nil - slog.Info("reloaded namespace authorization policy", slog.String("file", a.path)) - return a.allowed + slog.Info("loaded namespace authorization policy", slog.String("file", a.path)) + return a.allowed, nil +} + +// loadFailed records err as the load error and returns it. It logs err unless +// it repeats the last one or no policy has loaded yet, when +// LoadNamespaceAuthorizer returns it instead. The caller holds a.mu. +func (a *NamespaceAuthorizer) loadFailed(err error) error { + if a.allowed != nil && (a.loadErr == nil || a.loadErr.Error() != err.Error()) { + slog.Error("namespace policy file failed to load, keeping the last policy that loaded", slog.String("file", a.path), slog.Any("err", err)) + } + a.loadErr = err + return err } // LoadError reports why the policy file failed to load, or nil when the @@ -200,10 +203,8 @@ func (a *NamespaceAuthorizer) LoadError() error { if a == nil { return nil } - a.policy() - a.mu.Lock() - defer a.mu.Unlock() - return a.loadErr + _, err := a.policy() + return err } // Grants returns the loaded policy as atespace → sorted namespaces @@ -211,7 +212,7 @@ func (a *NamespaceAuthorizer) Grants() map[string][]string { if a == nil { return nil } - allowed := a.policy() + allowed, _ := a.policy() out := make(map[string][]string, len(allowed)) for atespace, namespaces := range allowed { list := make([]string, 0, len(namespaces)) @@ -228,7 +229,8 @@ func (a *NamespaceAuthorizer) Grants() map[string][]string { // deny: an atespace absent from the mapping, or a namespace not in its list, is // refused. func (a *NamespaceAuthorizer) Allowed(atespace, namespace string) bool { - set, ok := a.policy()[atespace] + allowed, _ := a.policy() + set, ok := allowed[atespace] if !ok { return false } @@ -241,7 +243,8 @@ func (a *NamespaceAuthorizer) Allowed(atespace, namespace string) bool { // before reading anything from Kubernetes. The namespace grant is checked again // here, so a request that straddles a reload is settled by the newer policy. func (a *NamespaceAuthorizer) AllowedSecret(atespace, namespace string, labels map[string]string) bool { - for _, g := range a.policy()[atespace][namespace] { + allowed, _ := a.policy() + for _, g := range allowed[atespace][namespace] { if len(g.labels) == 0 { return true }
atespaceallowed namespaces