Silent ServiceAccount fallback and missing ServiceAccount validation in Grafana k6 Operator can lead to privilege risk and TestRun initialization DoS.
Description
The K6 operator has a security vulnerability where it automatically falls back to the default ServiceAccount in two scenarios:
- When
spec.runner.serviceAccountName is omitted, the operator silently runs Pods with serviceAccountName: default.
- When an invalid or non-existent ServiceAccount is specified, the operator accepts the invalid value and creates a Pod that never progresses, leaving the TestRun stuck in the initialization phase.
The default ServiceAccount fallback vulnerability is still an issue because it breaks the security architecture, even though by default the ServiceAccount has limited permissions.
The fallback mechanism itself creates a security bypass that can be exploited, and there is no guarantee that the default ServiceAccount will always have safe permissions. Fixing the fallback behavior is required to maintain security controls and tenancy boundaries.
Endpoint / Vulnerable Component
https://github.com/grafana/k6-operator
Issue
- When no ServiceAccount is specified, the operator silently falls back to
default.
- When a non-existent ServiceAccount is specified, the operator accepts the invalid name and creates Pods that fail at runtime due to the missing ServiceAccount.
What Happens
- User creates a TestRun without specifying a ServiceAccount. The operator falls back to
default.
- User specifies a non-existent ServiceAccount. The operator accepts the invalid name, Pod creation fails at runtime, and the TestRun gets stuck in initialization.
Steps to Reproduce
Prerequisites
- Kubernetes cluster up and running
- k6 Operator deployed
Deploy k6 Operator bundle
curl https://raw.githubusercontent.com/grafana/k6-operator/main/bundle.yaml | kubectl apply -f -
Create a namespace
kubectl create ns k6-test-sa
Create custom ServiceAccount and RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
name: custom-test-sa
namespace: k6-test-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: custom-test-role
namespace: k6-test-sa
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: custom-test-rolebinding
namespace: k6-test-sa
subjects:
- kind: ServiceAccount
name: custom-test-sa
namespace: k6-test-sa
roleRef:
kind: Role
name: custom-test-role
apiGroup: rbac.authorization.k8s.io
Create test script ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: k6-test-script
namespace: k6-test-sa
data:
test.js: |
export const options = {
stages: [
{ duration: '30s', target: 5 },
{ duration: '1m', target: 5 },
{ duration: '30s', target: 0 },
],
};
export default function() {
console.log('Test running with custom ServiceAccount');
}
Validation
Test Case 1: Without ServiceAccount (Fallback Case)
Deploy TestRun without specifying ServiceAccount
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: test-without-sa
namespace: k6-test-sa
spec:
parallelism: 1
script:
configMap:
name: k6-test-script
file: test.js
runner:
# NO ServiceAccount specified - will fallback to "default"
resources:
limits:
cpu: "100m"
memory: "128Mi"
requests:
cpu: "50m"
memory: "64Mi"
Check ServiceAccount for TestRun without ServiceAccount
kubectl get pods -n k6-test-sa | grep test-without-sa
kubectl describe pod -n k6-test-sa <runner-pod-name> | grep -i "Service Account"
Observed Result
- Operator accepts the request with no validation and falls back to the default ServiceAccount.
Test Case 2: With Non-Existent ServiceAccount
Deploy TestRun with a non-existent ServiceAccount
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: test-with-nonexistent-sa
namespace: k6-test-sa
spec:
parallelism: 1
script:
configMap:
name: k6-test-script
file: test.js
runner:
serviceAccountName: non-existent-sa
resources:
limits:
cpu: "100m"
memory: "128Mi"
requests:
cpu: "50m"
memory: "64Mi"
Check status
kubectl get pods -n k6-test-sa -w
kubectl get testrun -n k6-test-sa
Observed Result
- Pod creation fails due to missing ServiceAccount.
- TestRun remains stuck in initialization indefinitely.
- Operator performs no dependency validation.
Vulnerable Code Snippet
File: cc-k6-operator/pkg/resources/jobs/runner.go
func NewRunnerJob(k6 *v1alpha1.TestRun) (*batchv1.Job, error) {
// CRITICAL SECURITY FLAW 1: Silent fallback to "default"
serviceAccountName := "default"
if k6.GetSpec().Runner.ServiceAccountName != "" {
serviceAccountName = k6.GetSpec().Runner.ServiceAccountName
}
// CRITICAL SECURITY FLAW 2: No validation that ServiceAccount exists
podSpec := corev1.PodSpec{
ServiceAccountName: serviceAccountName,
}
}
Impact
- Bypass of security controls through predictable fallback behavior.
- Potential privilege escalation when the default ServiceAccount is overly permissive.
- Broken multi-tenancy boundaries.
- Stuck TestRuns consume quota and human time.
- CI pipelines may block indefinitely due to initialization hangs.
Recommended Solution
Fix
Remove default ServiceAccount fallback and enforce validation.
func NewRunnerJob(k6 *v1alpha1.TestRun) (*batchv1.Job, error) {
if k6.GetSpec().Runner.ServiceAccountName == "" {
return nil, fmt.Errorf("spec.runner.serviceAccountName is required for security")
}
if err := validateServiceAccountNamespace(ctx, client, k6.Namespace, k6.GetSpec().Runner.ServiceAccountName); err != nil {
return nil, fmt.Errorf("invalid ServiceAccount: %v", err)
}
}
Add ServiceAccount validation
func validateServiceAccountNamespace(ctx context.Context, client client.Client, namespace, serviceAccountName string) error {
sa := &corev1.ServiceAccount{}
err := client.Get(ctx, types.NamespacedName{
Namespace: namespace,
Name: serviceAccountName,
}, sa)
if err != nil {
return fmt.Errorf("ServiceAccount %s not found in namespace %s: %v", serviceAccountName, namespace, err)
}
return nil
}
Silent ServiceAccount fallback and missing ServiceAccount validation in Grafana k6 Operator can lead to privilege risk and TestRun initialization DoS.
Description
The K6 operator has a security vulnerability where it automatically falls back to the
defaultServiceAccount in two scenarios:spec.runner.serviceAccountNameis omitted, the operator silently runs Pods withserviceAccountName: default.The default ServiceAccount fallback vulnerability is still an issue because it breaks the security architecture, even though by default the ServiceAccount has limited permissions.
The fallback mechanism itself creates a security bypass that can be exploited, and there is no guarantee that the default ServiceAccount will always have safe permissions. Fixing the fallback behavior is required to maintain security controls and tenancy boundaries.
Endpoint / Vulnerable Component
https://github.com/grafana/k6-operator
Issue
default.What Happens
default.Steps to Reproduce
Prerequisites
Deploy k6 Operator bundle
Create a namespace
Create custom ServiceAccount and RBAC
Create test script ConfigMap
Validation
Test Case 1: Without ServiceAccount (Fallback Case)
Deploy TestRun without specifying ServiceAccount
Check ServiceAccount for TestRun without ServiceAccount
Observed Result
Test Case 2: With Non-Existent ServiceAccount
Deploy TestRun with a non-existent ServiceAccount
Check status
Observed Result
Vulnerable Code Snippet
File:
cc-k6-operator/pkg/resources/jobs/runner.goImpact
Recommended Solution
Fix
Remove default ServiceAccount fallback and enforce validation.
Add ServiceAccount validation