From 206e26fdba591f2d1695dddd8c74c5ef09686655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 14:04:14 +0200 Subject: [PATCH] feat: when a vault refuses, what to check: PIM, the owner, or the network Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/azure.md | 6 ++++-- internal/provider/azure/azure_test.go | 14 ++++++++++++-- internal/provider/azure/convert.go | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/azure.md b/docs/azure.md index 753654d..2686d85 100644 --- a/docs/azure.md +++ b/docs/azure.md @@ -40,8 +40,10 @@ vault resources (e.g. *Reader*). Key Vault data-plane access is separate | read secret values | *Key Vault Secrets User* | | read certificate details | *Key Vault Certificate User* | -A missing permission is shown in the pane, including the action that was -denied. +A missing permission is shown in the pane, shortened to what was denied +(the action, or the access policy permission), with what to check: a +role or group to activate in PIM, access from the vault's owner, or, when +its firewall refused, the network you're on. ## Vaults you can't discover diff --git a/internal/provider/azure/azure_test.go b/internal/provider/azure/azure_test.go index e9497cd..62f783e 100644 --- a/internal/provider/azure/azure_test.go +++ b/internal/provider/azure/azure_test.go @@ -175,8 +175,11 @@ func TestForbiddenIsShortened(t *testing.T) { if err == nil { t.Fatal("expected error") } - if got, want := err.Error(), "403 Forbidden: Caller is not authorized to perform action on resource."; got != want { - t.Errorf("error = %q, want %q", got, want) + if got, want := err.Error(), "403 Forbidden: Caller is not authorized to perform action on resource.\nCheck: have you activated your role or group in PIM?"; !strings.HasPrefix(got, want) { + t.Errorf("error = %q, want %q…", got, want) + } + if hint := deniedHint("Client address is not authorized and caller is not a trusted service."); !strings.Contains(hint, "network") { + t.Errorf("firewall hint = %q", hint) } var re *azcore.ResponseError if !errors.As(err, &re) || re.StatusCode != 403 { @@ -184,6 +187,13 @@ func TestForbiddenIsShortened(t *testing.T) { } } +func TestShortenPolicyDenied(t *testing.T) { + msg := "The user, group or application 'appid=04b07795;oid=1234;numgroups=83;iss=https://sts.windows.net/t/' does not have secrets list permission on key vault 'kv-app;location=norwayeast'. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125287" + if got := shortenDenied(msg); got != "no secrets list permission in the vault's access policies" { + t.Errorf("shortenDenied = %q", got) + } +} + func TestShortenDenied(t *testing.T) { msg := "Caller is not authorized to perform action on resource.\r\nIf role assignments, deny assignments or role definitions were changed recently, please observe propagation time.\r\nCaller: appid=04b07795;oid=1234\r\nAction: 'Microsoft.KeyVault/vaults/secrets/readMetadata/action'\r\nResource: '/subscriptions/x/vaults/kv'" want := "Caller is not authorized to perform action on resource; missing Microsoft.KeyVault/vaults/secrets/readMetadata/action" diff --git a/internal/provider/azure/convert.go b/internal/provider/azure/convert.go index 038b316..f29fca0 100644 --- a/internal/provider/azure/convert.go +++ b/internal/provider/azure/convert.go @@ -11,6 +11,7 @@ import ( "fmt" "math/big" "net/http" + "regexp" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -282,10 +283,17 @@ func cleanError(err error) error { return &serviceError{status: re.StatusCode, msg: shortenDenied(msg), err: err} } +var policyDenied = regexp.MustCompile(`does not have (\w+ \w+) permission on key vault`) + // shortenDenied reduces Key Vault's RBAC denial, which runs to several // sentences plus caller and resource IDs, to its first sentence and the // missing action. func shortenDenied(msg string) string { + // Access policies: "The user, group or application 'appid=…;oid=…' + // does not have secrets list permission on key vault 'kv;location=…'". + if m := policyDenied.FindStringSubmatch(msg); m != nil { + return "no " + m[1] + " permission in the vault's access policies" + } const marker = "Action: '" i := strings.Index(msg, marker) if i < 0 { @@ -314,7 +322,21 @@ func (e *serviceError) Error() string { if e.msg != "" { s += ": " + e.msg } + if e.status == http.StatusForbidden { + s += "\n" + deniedHint(e.msg) + } return s } +// deniedHint is what to check when Key Vault refuses: the network, for +// its firewall's refusals, else access. On a line of its own, after the +// message. +func deniedHint(msg string) string { + if strings.Contains(msg, "Client address is not authorized") || strings.Contains(msg, "network access") || + strings.Contains(msg, "private link") { + return "Check: are you on a network the vault lets in (VPN, office)? Its firewall limits where it can be reached from." + } + return "Check: have you activated your role or group in PIM? Has the vault's owner given you access (a Key Vault role, or its access policies)?" +} + func (e *serviceError) Unwrap() error { return e.err }