Skip to content

Missing ServiceAccount validation and silent fallback to 'default' causes privilege risk & initialization DoS #702

@justmorpheus

Description

@justmorpheus

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)

Image

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

Image
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

Image
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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions