k8s Wrapper System for Unified Storage in Grafana: How RBAC Secures Kubernetes-Style Stores
Grafana implements a store wrapper in pkg/services/apiserver/auth/authorizer/storewrapper that intercepts calls to Kubernetes-style storage backends, swapping user identities for service identities while enforcing Grafana-specific authorization policies through the ResourceStorageAuthorizer interface.
The k8s wrapper system for unified storage allows Grafana to persist platform objects (dashboards, folders, alerts, and IAM resources) in a generic Kubernetes storage server while maintaining strict Grafana-specific RBAC controls. Located in the grafana/grafana repository, this wrapper acts as a security boundary between the API server and the underlying unified storage back-end, ensuring the generic gRPC-based resource server remains agnostic to Grafana's authorization model.
Core Architecture of the Store Wrapper
The wrapper system lives in pkg/services/apiserver/auth/authorizer/storewrapper and implements three fundamental concepts that decouple storage persistence from authorization logic.
The K8sStorage Interface
K8sStorage defines a narrow contract that mirrors the subset of the Kubernetes registry.Store used by Grafana. It declares methods for Create, Get, List, Update, Delete, and Watch operations. This interface allows the wrapper to treat any underlying storage implementation—from the unified gRPC resource server to in-memory test doubles—as a generic Kubernetes-style store.
ResourceStorageAuthorizer Hooks
ResourceStorageAuthorizer is an interface that enforces Grafana-level policy at specific interception points. Unlike standard Kubernetes authorizers that run before the request reaches storage, this authorizer receives the original user context (not the service identity used for the underlying store) and operates on the actual objects being persisted:
BeforeCreateandBeforeUpdate– Validate the object and user permissions before write operationsBeforeDelete– Authorize deletion attempts after fetching the target objectAfterGet– Filter or validate retrieved objects before returning them to the userFilterList– Post-process list results to hide items the user lacks permission to view
Identity Swapping Mechanism
The Wrapper struct implements rest.Storage and watch.Watcher interfaces. For every incoming request, it performs a critical context swap using identity.WithServiceIdentity() before delegating to the inner K8sStorage. This ensures the underlying unified storage back-end always operates under a trusted service account, while the authorizer evaluates policies against the original requesting user.
Storage Operation Flows
Each HTTP verb follows a specific authorization pipeline defined in pkg/services/apiserver/auth/authorizer/storewrapper/wrapper.go.
Create and Delete Operations
For Create requests, the wrapper first calls authorizer.BeforeCreate with the original user context to validate the operation and object. Only after authorization succeeds does it swap the context to a service identity using identity.WithServiceIdentity() and forward the request to the inner store.
For Delete operations, the wrapper performs a fetch using the service identity, runs authorizer.BeforeDelete on the original user context with the retrieved object, then forwards the delete command if authorized.
Read Operations (Get and List)
Get requests execute under a service identity to retrieve the raw object, then pass the result through authorizer.AfterGet with the original user context. This allows the authorizer to redact sensitive fields or reject the request based on the object's final state.
List operations fetch the complete result set using the service identity, then stream the list through authorizer.FilterList. The authorizer returns a potentially smaller list containing only objects visible to the requesting user, enabling row-level security without complex storage-layer filtering.
Watch Streams
When the underlying store implements watch.Watcher, the wrapper swaps the context to a service identity before calling Watch. This ensures the stream connects to the storage back-end with appropriate credentials, while the watch filter (if any) applies Grafana-specific visibility rules to the event stream.
Integration Points in Grafana
The wrapper is wired into Grafana's API registration system at multiple extension points, allowing different resource types to specify custom authorization logic.
IAM Resource Registration
During registration of the IAM API group, the store is explicitly wrapped with a type-specific authorizer. In pkg/registry/apis/iam/register.go (lines 74-76), the TeamBinding store is wrapped:
authzWrapper := storewrapper.New(teamBindingStore,
iamauthorizer.NewTeamBindingAuthorizer(b.accessClient))
storage[teamBindingResource.StoragePath()] = authzWrapper
This pattern ensures IAM resources enforce Grafana's RBAC model while persisting to the unified storage back-end.
App Installer Storage
The app installer in pkg/services/apiserver/appinstaller/server.go (lines 23-28) conditionally wraps stores when an app provides its own NamespaceScopedStorageAuthorizer:
if provider, ok := s.installer.(NamespaceScopedStorageAuthorizerProvider); ok {
authz := provider.GetNamespaceScopedStorageAuthorizer(gr)
if authz != nil {
return storewrapper.New(gs, authz)
}
}
This allows external applications to inject custom authorization logic while still benefiting from the unified storage infrastructure.
Default Deny Behavior
For cluster-scoped resources that do not supply an explicit authorizer, Grafana falls back to a DenyAuthorizer defined in pkg/services/apiserver/auth/authorizer/storewrapper/deny.go. This safe default rejects all mutating operations until a proper ResourceStorageAuthorizer is implemented, preventing unauthorized access to sensitive platform resources.
Implementing a Custom ResourceStorageAuthorizer
To enforce custom policies, implement the ResourceStorageAuthorizer interface and wrap your store during API registration. The following example restricts dashboard creation to users with the "Editor" role:
type DashboardAuthorizer struct{}
// Ensure interface compliance
var _ storewrapper.ResourceStorageAuthorizer = (*DashboardAuthorizer)(nil)
func (d *DashboardAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
if !identity.HasRole(ctx, "Editor") {
return storewrapper.ErrUnauthorized
}
return nil
}
func (d *DashboardAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
return nil // Allow all updates
}
func (d *DashboardAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
return nil // Allow all deletes
}
func (d *DashboardAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
return nil // Return object as-is
}
func (d *DashboardAuthorizer) FilterList(ctx context.Context, obj runtime.Object) (runtime.Object, error) {
return obj, nil // Return full list
}
Inject the authorizer when building the store:
wrapped := storewrapper.New(dashboardStore, &DashboardAuthorizer{})
For simple restrictions, embed storewrapper.NoopAuthorizer and override only specific methods:
type DenyDeleteAuthorizer struct{ storewrapper.NoopAuthorizer }
func (d *DenyDeleteAuthorizer) BeforeDelete(ctx context.Context, _ runtime.Object) error {
return storewrapper.ErrUnauthorized
}
Summary
- The k8s wrapper system in
pkg/services/apiserver/auth/authorizer/storewrapperenables Grafana to use generic Kubernetes-style storage while enforcing custom RBAC policies. - Identity swapping ensures the underlying unified storage back-end operates under a service identity, isolating it from Grafana's user authorization model.
- Five authorization hooks (
BeforeCreate,BeforeUpdate,BeforeDelete,AfterGet,FilterList) allow fine-grained control over CRUD and watch operations. - Integration occurs during API registration, with examples in
pkg/registry/apis/iam/register.goand the app installer system. - Safe defaults via
DenyAuthorizerprevent unauthorized access when explicit authorizers are not configured.
Frequently Asked Questions
How does the wrapper prevent privilege escalation to the storage backend?
The wrapper calls identity.WithServiceIdentity() to replace the user context with a trusted service identity before any call reaches the underlying K8sStorage. This ensures the unified storage gRPC server receives only service-level credentials, making it impossible for end users to bypass Grafana's authorization by directly interacting with the storage layer.
Can I use the wrapper with non-unified storage implementations?
Yes. The K8sStorage interface is storage-agnostic. You can wrap any implementation that satisfies the interface—including in-memory stores, SQL databases, or mock implementations—allowing you to test authorization logic without connecting to the full unified storage cluster.
What happens if an authorizer returns an error during a List operation?
If FilterList returns an error, the wrapper propagates that error to the API caller, resulting in a failed list request. To silently filter items without failing the entire request, the authorizer should remove unauthorized items from the list internally and return the reduced set with a nil error.
Where can I find the default deny-all authorizer implementation?
The DenyAuthorizer implementation resides in pkg/services/apiserver/auth/authorizer/storewrapper/deny.go. It implements ResourceStorageAuthorizer by returning ErrUnauthorized for all mutating operations, serving as a secure default for cluster-scoped resources until explicit authorization logic is provided.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →