{"record":{"id":"d3a3c9093359273a","repo":"grpc/grpc-go","slug":"rbac-header-matcher-for-q-is-q","errorCode":null,"errorMessage":"rbac: header matcher for %q is %q","messagePattern":"rbac: header matcher for %q is %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/rbac/rbac.go","lineNumber":174,"sourceCode":"\t\t}\n\tcase *v3rbacpb.Principal_OrIds:\n\t\tfor _, id := range p.OrIds.GetIds() {\n\t\t\tif err := normalizePrincipalHeaders(id); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase *v3rbacpb.Principal_NotId:\n\t\treturn normalizePrincipalHeaders(p.NotId)\n\t}\n\treturn nil\n}\n\n// normalizeHeaderMatcher rejects header matchers that A41 forbids (:scheme or a\n// grpc- prefixed name) and rewrites a \"host\" matcher to \":authority\".\nfunc normalizeHeaderMatcher(header *v3routepb.HeaderMatcher) error {\n\tname := header.GetName()\n\tif name == \":scheme\" {\n\t\treturn fmt.Errorf(\"rbac: header matcher for %q is %q\", name, \":scheme\")\n\t}\n\tif strings.HasPrefix(name, \"grpc-\") {\n\t\treturn fmt.Errorf(\"rbac: header matcher for %q starts with %q\", name, \"grpc-\")\n\t}\n\tif name == \"host\" {\n\t\theader.Name = \":authority\"\n\t}\n\treturn nil\n}\n\nfunc (builder) ParseFilterConfig(cfg proto.Message) (httpfilter.FilterConfig, error) {\n\tif cfg == nil {\n\t\treturn nil, fmt.Errorf(\"rbac: nil configuration message provided\")\n\t}\n\tm, ok := cfg.(*anypb.Any)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"rbac: error parsing config %v: unknown type %T\", cfg, cfg)\n\t}","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/rbac/rbac.go#L156-L192","documentation":"normalizeHeaderMatcher (rbac.go:173) rejects a header matcher whose name is \":scheme\". Per gRFC A41, RBAC policies must not match on the :scheme pseudo-header because grpc-go does not expose it as a mutable header.","triggerScenarios":"Any policy permission or principal (including those nested inside and/or/not rules) contains a HeaderMatcher with name == \":scheme\".","commonSituations":"Operator copies an Envoy RBAC config that matches on :scheme; policy templating that adds pseudo-headers generically.","solutions":["Remove the \":scheme\" header matcher from the RBAC policy.","If routing on scheme is required, handle it at the listener/LDS layer rather than in RBAC.","Review the policy for any pseudo-header (:path, :method, :scheme, :authority) usage against A41 rules."],"exampleFix":"// before\nprincipals: [{ identifier: { header: { name: \":scheme\", exact_match: \"https\" } } }]\n\n// after\n// (matcher removed; scheme handled at the listener layer)","handlingStrategy":"validation","validationCode":"func validatePolicyHeaders(p *v3rbacpb.Policy) error {\n    for _, pr := range p.GetPrincipals() {\n        if err := walkHeaders(pr, func(h *v3routepb.HeaderMatcher) error {\n            if h.GetName() == \":scheme\" { return errors.New(\":scheme forbidden by A41\") }\n            return nil\n        }); err != nil { return err }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"header matcher for\") {\n    // remove the :scheme matcher from the policy\n}","preventionTips":["Run policy authoring lint that rejects :scheme and other pseudo-headers.","Do not copy Envoy RBAC configs verbatim; audit them against gRFC A41."],"tags":["rbac","header-matcher","policy","xds","grpc"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}