Security¶
This document covers the security posture of the Superset Kubernetes Operator. It includes the threat model (trust boundaries, security assumptions, in-scope / out-of-scope concerns), secret handling, RBAC justification, security-relevant design decisions, and the vulnerability reporting process.
Trust Boundaries¶
The operator operates across three trust levels:
| Role | What they can do | Trust level |
|---|---|---|
| Cluster admin | Installs the operator, manages its RBAC and namespace | Full trust |
| Namespace admin | Creates and modifies Superset CRs in their namespace |
Trusted — can deploy arbitrary workloads |
| Superset end-user | Accesses the Superset web UI and API | Untrusted — no operator interaction |
Key assumption: Granting create or update on the Superset CRD is
equivalent to granting the ability to create Pods, Deployments,
Services (including NodePort and LoadBalancer if cluster policy allows),
ConfigMaps, ServiceAccounts, Ingresses, HTTPRoutes, NetworkPolicies,
HorizontalPodAutoscalers, PodDisruptionBudgets, and ServiceMonitors in that
namespace, including choosing container images, commands, arguments, environment
variables, volumes, and ServiceAccount references. This is inherent to the
Kubernetes operator pattern and is not a vulnerability.
A Superset CR controls all of the above plus arbitrary Python configuration
via spec.config. Restrict access to the supersets resource using Kubernetes
RBAC.
Supervised upgrade approval is object-local and uses the
superset.apache.org/approve-upgrade annotation on the Superset CR. This is
not a separate authorization boundary from Superset write access: anyone with
permission to patch or update the Superset resource can approve an image
change by setting the annotation to the approval token recorded in
status.lifecycle.upgrade.approvalToken. Clusters that need a distinct approver
role that cannot modify the parent CR spec should enforce that separation with
an external admission policy or a separate approval API.
Single Public CRD¶
The operator exposes one public custom resource, Superset. Component
Deployments, Services, ConfigMaps, HPAs, PDBs, lifecycle task Jobs, networking,
monitoring, and NetworkPolicies are reconciled as parent-owned Kubernetes
resources. The bundled superset-editor-role, superset-admin-role, and
superset-viewer-role therefore only need permissions for the supersets
resource and its status subresource.
ServiceAccount Selection Is Part of CR Write Access¶
CR authors can set serviceAccount.name with serviceAccount.create: false
to bind workloads to any existing ServiceAccount in the namespace — including
ServiceAccounts linked to cloud IAM or workload-identity setups (IRSA, GKE
Workload Identity, Azure AD). This is intentional and enables legitimate
integration patterns, but it means a CR author inherits whatever permissions
the selected ServiceAccount has. Cluster and namespace admins should treat
"can create Superset CRs" as equivalent to "can run workloads under any
ServiceAccount in this namespace" and restrict ServiceAccount distribution
accordingly.
Security Model¶
Production/Staging vs Development Mode¶
The operator enforces a strict separation between hardened modes (Production and Staging) and Development:
- Production/Staging (
environment: Productionis the default;Stagingkeeps the same secret rules but allows the destructiveclonetask): InlinesecretKey,previousSecretKey,metastore.uri,metastore.password,valkey.password, websocketconfig, andlifecycle.clone.source.passwordare rejected by CRD CEL validation rules. Users reference secrets viasecretKeyFrom,previousSecretKeyFrom,metastore.uriFrom,metastore.passwordFrom,valkey.passwordFrom, websocketconfigFrom, andlifecycle.clone.source.passwordFrom— the operator wires each of these asvalueFrom.secretKeyRefenv vars (or, for websocketconfigFrom, mounts the referenced Secret key as a file). - Development (
environment: Development): Inline secrets are allowed for local development convenience. Additionally,lifecycle.init.adminUserandlifecycle.init.loadExamplesare permitted — these create a default admin account and load sample data during initialization. Admin credentials fromadminUserare stored as plain-text environment variables on the parent Superset CR and the resulting task Pod spec (visible to anyone with read access to these resources in the namespace). The admin password also appears in the Pod's process arguments via shell expansion.
Development mode is intentionally less secure. It exists for local development with Kind or Minikube where secret management infrastructure is not available.
Secret Handling¶
In Staging and Production modes, secrets follow this path:
- User creates a Kubernetes
Secretcontaining the secret key and database credentials - User references the Secret via
secretKeyFrom,previousSecretKeyFrom,metastore.uriFrom,metastore.passwordFrom,valkey.passwordFrom,lifecycle.clone.source.passwordFrom, orwebsocketServer.configFromon the Superset CR. For env-var references the operator injectsvalueFrom.secretKeyRef; forwebsocketServer.configFromthe operator mounts the referenced Secret key as a file. In every case the secret value is resolved by the kubelet at pod startup, not by the operator. - The operator generates
superset_config.pythat rendersSECRET_KEY = os.environ['SUPERSET_OPERATOR__SECRET_KEY']— the actual secret value is resolved at Python runtime from the env var, so it never appears in the ConfigMap - The operator does not need Kubernetes Secret read permissions for this flow and never reads, logs, writes, or stores secret values in ConfigMaps, CRD status fields, or Events
Task failure caveat: When a task Job fails, the operator records a truncated failure message (max 256 characters) in the parent Superset status and Kubernetes Events for debugging. If the task command writes sensitive data to its failure output (e.g., a database connection error that includes credentials), a truncated form may appear in status. This only applies to the task container's own output, not to operator-managed secret references.
Scope of this guarantee: The above applies to operator-managed secret
references (secretKeyFrom, previousSecretKeyFrom, metastore.uriFrom,
metastore.passwordFrom, valkey.passwordFrom,
lifecycle.clone.source.passwordFrom, and websocketServer.configFrom).
User-authored fields — raw Python in spec.config, component-level config,
bootstrapScript, and podTemplate.container.env — are trusted input and may
contain arbitrary values including secrets. Users with read access to Superset
CRs or the generated ConfigMaps will see any values placed in these fields.
These are out of scope for the operator's secret handling guarantees
(see What Is Generally Out of Scope).
Raw Python Configuration¶
The spec.config field accepts arbitrary Python code that is appended to the
generated superset_config.py. This is by design — Superset's configuration
system is Python-based and requires arbitrary Python for features like custom
security managers, database drivers, and feature flags. The same trust model
applies to bootstrapScript, custom lifecycle commands, container env vars,
and mounted files: operator-managed secret transport avoids ConfigMap leakage,
but user-supplied raw fields can still expose secrets if CR authors put secrets
there directly.
Since CR creators can already deploy arbitrary containers (via image,
command, args), the ability to inject Python does not expand the attack
surface. Restrict who can create Superset CRs using Kubernetes RBAC.
CRD Validation¶
All validation is enforced via CEL
rules embedded in the CRD schema (x-kubernetes-validations). No admission
webhooks are used. This avoids the operational complexity of cert-manager and
ensures validation is always active regardless of how the operator is deployed.
Key rules:
- Production/Staging secret rejection: Inline
secretKey,metastore.uri,metastore.password,valkey.password, and websocketconfigare rejected outside Development mode - Staging clone boundary:
lifecycle.cloneis allowed only in Development or Staging because it performs a destructive target database drop - Mutual exclusivity:
secretKey/secretKeyFrom, metastore URI vs structured fields, Valkey password inline vs Secret reference, websocketconfig/configFrom, gateway vs ingress - Ingress requires webServer:
spec.networking.ingressrules target the web server service, so a CR cannot enable Ingress withoutwebServer - Gateway requires at least one routable component:
spec.networking.gatewayroutes to whichever ofwebServer,websocketServer,mcpServer, andceleryFlowerare configured; a CR with only Beat and Worker cannot enable Gateway - Monitoring requires webServer: ServiceMonitor scrapes the web server service
- Defaulting:
environmentdefaults toProduction, image repository and pull policy default via kubebuilder markers
CEL is the operator's built-in validation layer. Cluster operators who want
defense-in-depth — for example, restricting which image.repository values are
allowed, forbidding environment: Development outside specific namespaces, or
requiring particular labels on every Superset CR — can layer a policy engine
such as Kyverno, OPA Gatekeeper, or the Validating Admission Policy API on top
of these CRD-level rules.
Design Decisions¶
The following design choices are intentional and documented here to avoid repeat review cycles:
- Ingress
CreateOrUpdatereplaces the full spec. ThereconcileIngressmutate function assignsingress.Spec = networkingv1.IngressSpec{...}before building rules, so rules are rebuilt from scratch on every reconcile — they do not accumulate. setConditionignores message-only changes. Condition updates are triggered by Status, Reason, or ObservedGeneration changes. In all current call sites, Status and Reason change together, so message-only changes are a no-op by design.LastTransitionTimeis only updated when Status changes (per Kubernetes API conventions), not on Reason or generation changes alone.FlatComponentSpecis shared across component Deployments and lifecycle tasks. Lifecycle tasks use Jobs (no Deployment), so fields like Autoscaling, PDB, and Replicas are unused. The parent controller nils these fields before creating task Jobs. A dedicatedFlatInitSpecmay be introduced in a future API version, but the shared struct avoids duplicating Image, PodTemplate, and ServiceAccountName today.computeChecksumhas an unreachable fallback. Thefmt.Sprintf("%v")fallback afterjson.Marshalcannot fire for CRD types (which always marshal successfully). It exists as a defensive guard, not as an expected code path.- WebServer and McpServer share port 8088. These are separate Pods and Services, so identical port numbers do not conflict.
- Generated Python and bootstrap wrapping use operator-controlled values. String fields
interpolated into
superset_config.py(key prefixes, SSL cert paths) come from CRD fields whose values are set by CR authors — trusted actors who can already deploy arbitrary containers. Raw Python inspec.configand shell inbootstrapScriptare appended verbatim by design and are out of scope (see "What Is Generally Out of Scope" below). - Celery Flower uses shell expansion for
--url_prefix. The Flower default command uses/bin/sh -cto expand$SUPERSET_OPERATOR__FLOWER_URL_PREFIX, which is set fromservice.gatewayPath. ThegatewayPathfield is restricted to^/[a-zA-Z0-9/_.-]+$by CRD validation, preventing shell metacharacter injection. Additionally, CR creators can already override the command entirely viapodTemplate.container.command, so this does not expand the attack surface. - ServiceAccount ownership. When
serviceAccount.createis true (the default), the operator creates and owns the ServiceAccount. If a ServiceAccount with the specified name already exists and is not owned by the Superset CR, the operator refuses to adopt it and reports an error. Users who want to reference a pre-existing ServiceAccount should setcreate: false. - Managed resource adoption and cleanup. The operator reconciles managed resources at deterministic names derived from the Superset name. Kubernetes controller-owner semantics prevent adopting resources already controlled by another controller. Unowned resources with the same managed name, or resources carrying the operator's cleanup labels, may be adopted or deleted during reconciliation. This is within the trust model: users who can create or update Superset CRs are trusted namespace operators and already have the effective ability to manage the corresponding workloads and resources.
- Supervised upgrade approval uses a consumed, target-bound annotation. In
supervised mode, an image change is gated on the
superset.apache.org/approve-upgradeannotation matching the approval token recorded instatus.lifecycle.upgrade.approvalToken. The token is derived from the observed source and target image refs, so a later spec update changes the token and requires a separate approval. After lifecycle completion is persisted to status, the operator deletes the annotation so a stale approval cannot authorize a later image change. Kubernetes RBAC cannot grant metadata-only patch access, so the manager haspatchonsupersets; the implementation uses that permission only to remove the approval annotation from the parent resource. - NetworkPolicy provides baseline ingress segmentation, not egress restriction. When the built-in NetworkPolicy is enabled, the operator installs policies that isolate ingress between Superset instances and allow external clients to reach user-facing components (web, websocket, flower, MCP). Egress is intentionally unrestricted so workloads can reach the metastore database, Valkey, SMTP servers, object stores, and any other user-configured dependencies. Users who require strict egress isolation should disable the built-in policy and author their own.
- Metrics endpoint ships with a permissive TLS default. The bundled
ServiceMonitor defaults to
insecureSkipVerify: trueagainst the manager's self-signed serving certificate so that Prometheus can scrape metrics out-of-the-box on clusters without cert-manager. Authentication and authorization are still enforced via bearer tokens validated byTokenReview/SubjectAccessReview(see RBAC Justification), so the endpoint is not anonymously accessible. Production deployments should switch to cert-manager-issued certificates and setinsecureSkipVerify: false—charts/superset-operator/values.yamldocuments the flip.
RBAC Justification¶
The operator runs with a ClusterRole to support managing Superset instances
across namespaces. Each permission is justified below:
| Resource | Verbs | Reason |
|---|---|---|
configmaps |
CRUD | Stores generated superset_config.py per component |
services |
CRUD | Exposes web server, Flower, websocket, MCP server |
serviceaccounts |
CRUD | Creates per-instance ServiceAccount for pod identity |
pods |
get, list, watch | Reads Job pods to verify drain progress and component readiness |
jobs |
CRUD | Manages deterministic lifecycle task Jobs |
events |
create, patch, update | Records reconciliation events |
deployments |
CRUD | Manages component Deployments |
horizontalpodautoscalers |
CRUD | Manages HPA for scalable components |
poddisruptionbudgets |
CRUD | Manages PDBs for availability |
ingresses, networkpolicies |
CRUD | Optional networking features |
httproutes |
CRUD | Optional Gateway API support |
servicemonitors |
CRUD | Optional Prometheus integration |
tokenreviews, subjectaccessreviews |
create | Metrics endpoint auth/authz (controller-runtime secure metrics) |
supersets |
get, list, watch, patch | Reads Superset CRs and patches metadata to consume target-bound supervised-upgrade approval; Kubernetes RBAC is not field-scoped |
supersets/status |
get, update, patch | Updates reconciliation status only |
The operator does not request:
*(wildcard) on any resource or verbimpersonateor RBAC management permissionscluster-adminor equivalent- Kubernetes Secret read or write permissions
Install Scope¶
The operator supports two install modes, selectable at deploy time:
- Cluster-scoped (default). The manager ServiceAccount is bound to the
generated
ClusterRole(manager-role) via aClusterRoleBinding(manager-rolebinding), and the cache watches every namespace. Appropriate when a cluster admin administers the operator centrally. Helm:watch.scope: cluster. - Namespace-scoped. The manager watches only the namespaces listed in
WATCH_NAMESPACE(comma-separated). Appropriate for restricted clusters that forbidClusterRolecreation, or for single-tenant installs that want a tighter blast radius.
Leader election is namespace-scoped in both modes: the operator binds the
namespace-local Role leader-election-role to the manager ServiceAccount via
the RoleBinding leader-election-rolebinding in the operator's own
namespace, and the lease/lock objects live there too.
The RBAC shape differs between Helm and Kustomize for namespace-scoped installs:
- Helm (
watch.scope: namespaces) renders oneRoleand oneRoleBindingper watched namespace, and does not create a managerClusterRole/ClusterRoleBindingat all. With CRDs preinstalled by a cluster admin andmetrics.enabled: false, this install succeeds on clusters that deny cluster-scoped RBAC to the installer (see the Constraints list below). - Kustomize (
config/components/watch-namespace/) retains the controller-gen–generatedClusterRolebut replaces theClusterRoleBindingwith a namespacedRoleBindingpointing at that sameClusterRole. ARoleBinding→ClusterRolepairing restricts the granted permissions to the binding's namespace. The Kustomize path therefore still requires cluster-scoped RBAC at install time for theClusterRole; its runtime footprint is namespace-scoped.
Constraints common to both paths:
- CRD installation always needs cluster-admin. CRDs are cluster-scoped resources; watch-scope does not change that.
- Secure metrics auth still needs cluster-scoped RBAC. The metrics
endpoint uses
TokenReview/SubjectAccessReview, which are cluster-level APIs. On clusters that forbidClusterRoleentirely, disable metrics (metrics.enabled: falsein Helm values). - Changing the watched-namespace list requires a manager restart. The manager cache is built at startup; dynamic reconfiguration is not supported.
- Superset CRs in unwatched namespaces are silently ignored. The
operator logs the watched set at startup but does not detect stray CRs
elsewhere — confirm by tailing the startup log or listing
Supersetsacross namespaces manually if users report missing reconciliation.
What Is In Scope¶
The following are valid security concerns for this project:
- Secrets leaking into ConfigMaps, Events, logs, or CRD status in Staging or Production mode
- Privilege escalation via the operator's RBAC permissions
- CRD validation bypass (e.g., crafting a CR that evades CEL rules)
- The operator container's own security posture (it runs as non-root, read-only, all capabilities dropped)
- Supply chain issues in the operator's build and release pipeline
- Vulnerabilities in the operator binary itself
Note on workload security contexts: The operator does not enforce default
security contexts on Superset workload pods — it propagates whatever the user
configures via podTemplate.podSecurityContext and
podTemplate.container.securityContext. The
production sample
shows recommended settings. Workload pod security is the user's responsibility
(see What Is Generally Out of Scope).
Recommendation — Pod Security Admission: The operator manager Pod is
configured to satisfy the restricted Pod Security Standard (non-root,
read-only root filesystem, all capabilities dropped, seccompProfile:
RuntimeDefault, allowPrivilegeEscalation: false). For defense in depth,
label the operator's namespace with
pod-security.kubernetes.io/enforce: restricted so the apiserver rejects any
Pod that drifts from this baseline.
Supply Chain¶
The release pipeline produces signed multi-architecture artifacts:
- Base images: the build stage uses a fully-qualified Go patch tag and the
runtime stage uses
gcr.io/distroless/static:nonroot— no shell, no package manager, no unnecessary binaries. The Dockerfile pins both by digest and Renovate keeps them current (digest refreshes are applied without the version soak — see the dependency policy below). - Architectures:
linux/amd64andlinux/arm64are built and signed identically. - Image signatures: The release workflow signs the manager image and the
packaged Helm chart with Cosign using
GitHub Actions OIDC (keyless). Verify with
cosign verify ghcr.io/apache/superset-kubernetes-operator@<digest>. - Dependency policy: Go modules, GitHub Actions, and container base images are
kept current via Renovate with pinned versions and
a 7-day minimum age before a version update is proposed. This soak does not
apply to digest updates: when a floating tag is re-pushed to a new digest for the
same version (a base-OS rebuild, or a patch published under a rolling tag), Renovate
proposes the digest bump immediately, because
minimumReleaseAgegates versions, not digests. The Go builder is pinned to a fully-qualified patch version so Go upgrades are soaked version updates; the distroless runtime base has no semantic-version tag, so its digest refreshes are inherently un-soaked. This residual exposure is accepted and bounded by the minimal distroless base and by keyless Cosign signing of the image this project itself publishes. - SBOM & provenance: Each published image carries a per-platform Software
Bill of Materials (SPDX) and SLSA build provenance, attached as in-toto
attestations by BuildKit at build time. Inspect them with
docker buildx imagetools inspect ghcr.io/apache/superset-kubernetes-operator:<tag> --format '{{ json .SBOM }}'(or.Provenance), or withcosign download attestation. - Vulnerability scanning: CI runs
govulncheckagainst the Go source (reachable CVEs in dependencies and the standard library) and Trivy against the built image (OS and shipped-binary CVEs), with results uploaded to GitHub code scanning. CodeQL provides static analysis of the Go code. - Supply-chain posture: An OpenSSF Scorecard workflow runs on a schedule and publishes the project's score (linked from the README badge).
What Is Generally Out of Scope¶
The following areas are usually outside this project's security scope. Reports are still welcome when they show that the operator changes the expected trust boundary, weakens Kubernetes controls, leaks operator-managed secrets, or makes one of these conditions materially worse:
- Superset application vulnerabilities — report these to the Apache Superset project
- Database or cache security — the operator does not manage PostgreSQL or Valkey instances; their security is the user's responsibility
- Kubernetes control plane vulnerabilities — report these to the Kubernetes security team
- Development mode allows inline secrets — this is intentional and documented for
local development; Production mode is the enforced default.
lifecycle.init.adminUserandlifecycle.init.loadExamplesare also Development-only features, rejected by CRD validation in Staging and Production - CR creators can deploy arbitrary workloads — creating or updating any Superset CRD is equivalent to creating Pods with chosen images, commands, env vars, volumes, and ServiceAccounts; this is inherent to the operator pattern and is the expected trust model (see Trust Boundaries)
- Arbitrary Python via
spec.config— this field accepts raw Python by design; CR creators can already deploy arbitrary containers, so Python configuration does not expand the attack surface - Lifecycle clone task command is trusted input — the
lifecycle.clonetask runs whatever image and command the CR author configures, so shell and SQL content embedded in that command is trusted input. CR authors already deploy arbitrary containers, so the clone task does not expand the attack surface. Review clone commands as part of CR review, not as a separable vulnerability class. - Container image vulnerabilities — the operator does not control the contents of the Superset container image
- Workload pod security contexts — the operator propagates user-configured security contexts but does not enforce defaults; workload pod hardening is the user's responsibility (see the production sample for recommended settings)
- Websocket server is experimental and pending security hardening — the
websocketServercomponent is not yet well supported and may exhibit gaps, either in the operator (e.g. unvalidated gateway/ingress routing) or upstream in the Node.js websocket image, which is community-maintained and not part of the default Superset image. Treat it as subject to change and avoid enabling it in production until it is hardened. Note that in Development mode the inlinewebsocketServer.configis written to a ConfigMap — the one place an inline secret (e.g. the websocket JWT secret) legitimately lands in a ConfigMap. Staging and Production reject inlineconfigand requirewebsocketServer.configFrom, which mounts the referenced Secret as a file. - Network-level attacks (MITM, DNS spoofing) — these are infrastructure concerns outside the operator's control
- Missing features (e.g., "should support Vault integration") — these are normally handled as feature requests unless the missing behavior creates a concrete security regression in the operator
Reporting Vulnerabilities¶
The Apache Superset Kubernetes Operator project follows the Apache Software Foundation vulnerability handling process.
To report a security vulnerability, please email security@apache.org.
Please do not file a public GitHub issue for security vulnerabilities.
Supported Versions¶
| Version | Supported |
|---|---|
| v1alpha1 (latest) | Yes |
Component Scope¶
This policy covers the Superset Kubernetes Operator and its components:
- CRD definitions and CEL validation rules
- Controller reconciliation logic
- RBAC and resource management
- Helm chart and deployment manifests
The websocketServer component is experimental and pending security hardening
(see What Is Generally Out of Scope); its
guarantees are best-effort until that work is complete.