Skip to content

Adding one capability silently strips the runtime class defaults, breaking stock images #312

Description

@scotwells

What a customer hits

A customer has a working sandbox workload on the general-purpose runtime class. They need one extra capability, so they add the obvious thing:

securityContext:
  capabilities:
    add: [NET_ADMIN]

They get NET_ADMIN — and silently lose the ten capabilities the class was granting them, including CHOWN, SETGID, SETUID, and NET_BIND_SERVICE. A stock nginx or Postgres image then fails at runtime: its entrypoint chowns data directories and its workers drop privileges. The failure surfaces far from the one-line YAML change that caused it, and the field they edited is named add, which in Kubernetes is additive.

It gets worse: per datum-labs/kata-provider#18 the resulting crash-loop still reports Available=True, so the customer gets no signal from the platform at all.

Found while validating #310.

Evidence

Verified against the staging datum-cloud project control plane with kubectl apply --dry-run=server (real webhooks, nothing created).

Container with no securityContext — admission stamps:

capabilities:
  add: [CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, NET_BIND_SERVICE, SETFCAP, SETGID, SETPCAP, SETUID]
  drop: [ALL]
allowPrivilegeEscalation: false
seccompProfile: {type: RuntimeDefault}

Same container stating capabilities: {add: [NET_ADMIN]} — admission stores:

capabilities:
  add: [NET_ADMIN]
  drop: [ALL]

All ten defaults are gone. A second probe confirms there is no merge anywhere: input add: [NET_ADMIN, CHOWN] stores exactly those two.

Reproduction

Against a control plane with the RuntimeClasses feature gate enabled and the general-purpose class published:

apiVersion: compute.datumapis.com/v1alpha
kind: Workload
metadata:
  name: cap-repro
spec:
  template:
    spec:
      runtime:
        class: general-purpose
        resources:
          instanceType: datumcloud/d1-standard-2
        sandbox:
          containers:
            - name: app
              image: docker.io/library/nginx:latest
              # probe 2: uncomment
              # securityContext:
              #   capabilities:
              #     add: [NET_ADMIN]
      networkInterfaces:
        - network:
            name: default
  placements:
    - name: us
      locations:
        - name: gcp-us-south1-a
      scaleSettings:
        minReplicas: 1
kubectl apply --dry-run=server -f workload.yaml -o json

Run once as written, once with the securityContext block uncommented, and compare the stored securityContext on the container.

The design decision this has to overturn

This behaviour is deliberate today. The reasoning we published was that a customer statement is the whole answer for the capabilities it covers: merging the class default into a stated set would put back the invisible injection the runtime class surface was meant to replace, so a container that states capabilities keeps exactly those.

That rationale no longer holds. The concern it protects against is the platform adding capabilities invisibly — but admission now stamps the resolved result onto the stored workload, so a merged set would be fully visible to the customer in their own spec, alongside the drop: [ALL] that is already appended to stated sets for exactly this legibility reason. Stamping already solves invisibility. Merging costs nothing in legibility, and replacement costs the customer a working deployment.

Expected

A stated capability should be additive to the class default, or the platform must make the replacement explicit to the customer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions