Findings from a security review of the base container images, Helm charts, CI workflows, and configuration.
Each Containerfile now creates a dedicated service user with configurable UID/GID (ARG SERVICE_UID/SERVICE_GID) and sets USER to that service user. Entrypoints include a gosu guard: when Docker Compose overrides user: "0:0" to fix bind-mount ownership, the entrypoint drops back to the service user via gosu. In Kubernetes, podSecurityContext with runAsUser and runAsNonRoot: true enforces non-root directly, bypassing the gosu path.
- yamcs (10001), openmct (10002), jupyter (10003), jsle (10004)
containers/jupyter/jupyterhub_config.py sets authenticator_class = 'dummy' with password = 'password'. This is baked into the image. Even though the comment says "development", the image ships to GHCR and can be deployed as-is.
containers/jupyter/jupyterhub_config.py passes --allow-root to spawned notebooks. Resolved by C1: the Containerfile sets USER 10003:10003, so the spawner is never root and --allow-root is a no-op. The flag can be removed as cleanup.
Removed sudo from both apt-get blocks in containers/openmct/Containerfile.
All base image tags are mutable with no digest pinning:
FROM maven:3.9.9-eclipse-temurin-17FROM ubuntu:25.04FROM quay.io/jupyterhub/jupyterhub:5
A supply-chain compromise of any upstream tag silently propagates. Pin by @sha256: digest.
All Containerfiles default to GIT_COMMIT=master. Cloning master means builds are non-reproducible and a compromised upstream commit is pulled silently. Default to a pinned tag or commit SHA.
containers/yamcs/Containerfile—GIT_COMMIT=mastercontainers/sle/Containerfile—GIT_COMMIT=mastercontainers/openmct/Containerfile—GIT_COMMIT=master
containers/openmct/Containerfile uses curl -o- ${NVM_URL} | bash to install nvm. NVM_VERSION is pinned, but the download has no checksum verification.
All four Helm deployment templates allow the container to write anywhere on its filesystem. Writable root filesystems let an attacker drop binaries, modify configs, or persist changes.
All Helm values.yaml now include podSecurityContext with runAsUser, runAsGroup, fsGroup, and runAsNonRoot: true. Deployment templates wire this via {{- with .Values.podSecurityContext }}.
No chart provides a NetworkPolicy. All pods can communicate with every other pod in the namespace and potentially the entire cluster.
MAVEN_HTTPS_PROXY, HTTPS_PROXY, HTTP_PROXY are persisted as ENV in all Containerfiles. If these contain credentials (e.g., http://user:pass@proxy:8080), they are visible via docker inspect, docker history, and the Kubernetes pod spec. Use build-time-only ARG without ENV persistence, or inject at runtime via Secrets.
All four charts set defaultMode: 0755 on the entrypoint ConfigMap. This makes the script world-readable and world-executable. Use 0550 (owner+group execute, no world access).
None of the deployments set seccompProfile: RuntimeDefault. Containers run without seccomp filtering unless the cluster enforces a default.
Only dist/ and charts/ are excluded. No exclusions for .env, *.pem, *.key, settings.xml (which could contain proxy credentials), IDE files, or OS artifacts.
containers/jupyter/Containerfile installs all pip and gem packages without version pins (jupyterlab, iruby, etc.). A compromised or yanked package version gets pulled into the image.
.github/workflows/publish-charts.yaml — the lint job inherits the default GITHUB_TOKEN permissions. Should explicitly set contents: read to follow least-privilege.
vim, tmux, tree, wget, curl, iputils-ping are installed in all images. These expand the attack surface. Consider a multi-stage build that excludes dev tools from the final image.
containers/openmct/entrypoint.sh and containers/jupyter/entrypoint.sh keep the container alive after the main process exits. This masks crashes and leaves a shell available via kubectl exec (as the service user, not root, since C1 was resolved).
No Trivy, Grype, or Snyk step in build-images.yaml. Vulnerabilities in base images and dependencies are not caught before publishing.
.github/workflows/build-images.yaml has a cron rebuild that picks up upstream changes, but there is no diff or vulnerability report, so breakage or new CVEs go unnoticed.
| Severity | Count | Key theme |
|---|---|---|
| Critical | 3 | Root containers, hardcoded credentials, root notebooks |
| High | 6 | sudo, unpinned images/sources, curl|bash, missing pod security |
| Medium | 7 | No NetworkPolicy, env credential leak, unpinned deps, seccomp |
| Low | 4 | Dev tools in prod, masked crashes, no image scanning |
C1, C3, H1, and H6 have been resolved. The remaining highest-impact fix is C2: replacing the dummy JupyterHub authenticator with a real one.