canette
Security

Build security

Security properties of the canette build pipeline and what risks remain.

canette builds container images by running untrusted code inside your cluster. This page explains what protections are in place and what risks remain — so you can make an informed decision about what code you allow to be built.

Building code you do not control or have not reviewed carries inherent risk. The protections described here reduce the attack surface significantly, but they do not eliminate it. Treat canette like any other CI system: only build repositories you trust.

What canette does to contain builds

Dedicated build namespace

All build jobs run in the canette-build namespace, isolated from application namespaces and the canette-system control plane namespace.

Non-root containers

Both the git-clone and image-build containers run as UID/GID 1000 (canette user). The pod security context enforces this:

  • runAsNonRoot: true — Kubernetes rejects the pod if the image tries to run as root
  • runAsUser: 10001 / runAsGroup: 10001 — explicit UID/GID
  • fsGroup: 10001 — shared volume (/workspace) is owned by the build user in both containers
  • allowPrivilegeEscalation: false — setuid binaries cannot grant elevated privileges

No Kubernetes API access

Build pods have automountServiceAccountToken: false. The build containers have no credentials to call the Kubernetes API, so a compromised build cannot enumerate or modify cluster resources.

Seccomp profile

The build pod runs with seccompProfile: RuntimeDefault. The actual image building (user namespaces, overlayfs) happens inside the separate, long-lived buildkitd deployment — the build job itself only communicates with buildkitd over TCP and needs no special syscalls.

Hard time limit

Each build job has a 30-minute activeDeadlineSeconds. A hung or deliberately slow build is forcibly terminated by Kubernetes.

Isolated railpack prepare subprocess

When Railpack is used to auto-detect the project type, it runs in a subprocess with a 5-minute timeout and a minimal, clean environment — only PATH, HOME, and TMPDIR are set. Job-level environment variables (app secrets, CANETTE_CONFIG, build credentials) are not inherited by the Railpack process.

Credential handling

  • PAT (HTTPS): the access token is embedded in the clone URL for the duration of the git clone command only. It is never written to disk.
  • SSH keys: GIT_SSH_COMMAND is set to point directly at the mounted credential secret volume. The private key is never copied to the container filesystem.
  • Git credentials are deleted from the cluster after each build completes (or fails).

Input validation

All inputs to the git-clone and image-build containers are validated before any subprocess is launched:

  • Git URLs must be https:// or git@host:path — no other schemes
  • Git refs are validated against a strict allowlist (alphanumeric, ., -, _, /)
  • App paths are checked against path traversal attacks
  • canette.yaml build args are validated for shell metacharacters and size

Risks to be aware of

The protections above reduce but do not eliminate risk. The following attack surfaces remain.

Malicious configuration

The user can provide a canette.yaml or Dockerfile in the committed repository. These files can override build settings — including the Dockerfile path and build context. A deliberately crafted Dockerfile can:

  • Install arbitrary software in the resulting image
  • Exfiltrate secrets passed as build args during the build
  • Produce an image that runs malicious code when deployed

canette validates the syntax and structure of canette.yaml but does not analyse the content of Dockerfiles or build scripts for malicious intent. This is equivalent to running any CI pipeline against untrusted code.

Railpack auto-detection and railpack.json

When no Dockerfile is present, Railpack analyses the repository content to generate a build plan. A repository could contain files crafted to influence Railpack's detection logic or the generated build plan. While the Railpack subprocess runs in a clean environment with limited privileges, the analysis itself reads arbitrary files from the repository.

To prevent this, canette refuses to build if a railpack.json is present in the app directory. The build fails immediately with a clear error — remove the file from the repository to proceed. Railpack auto-detection (reading package.json, pyproject.toml, and similar project files) is unaffected; only the explicit Railpack config override is blocked.

If Railpack does not produce a working image for your project, add a Dockerfile instead — it takes precedence over Railpack and eliminates any need for railpack.json. See Build pipeline.

Build-time network access

Build jobs have outbound internet access (required to fetch dependencies). During a build, the build process can make arbitrary outbound network requests — for example, to exfiltrate environment variables or source code, or to reach internal services on your network if your cluster does not enforce egress restrictions.

Mitigation: Configure network policies on your cluster to restrict egress from the canette-build namespace to only what is needed (package registries, your git host, your image registry).

Image content

canette builds and deploys whatever the build produces. A malicious or vulnerable image runs inside your cluster with the same privileges as any other app.

Vulnerability scanning is available via the security.scan Helm values. The active provider is selected automatically from the registry URL:

  • ECR registry — ECR basic scanning is used. The builder calls StartImageScan after each push and polls DescribeImageScanFindings. Scanning is enabled by default (free, no extra infrastructure).
  • Other registries — Trivy runs as a K8s Job in the build namespace. Scanning is disabled by default (adds build time; opt in via security.scan.enabled: "true"). SBOM generation (CycloneDX) is a separate opt-in via security.scan.sbomEnabled: true — it requires a second Trivy pass and adds ~30–60 s.

Set security.scan.mandatory: true to block deployments when findings at or above security.scan.failSeverity are found.

Shared buildkitd

All builds share a single buildkitd daemon. Build cache is shared between builds. A build that intentionally or accidentally corrupts the BuildKit cache could affect subsequent builds. The buildkitd pod is a high-value target and should not be exposed outside the cluster.

Rootless mode (default): buildkitd runs as UID 1000 with seccompProfile: Unconfined and appArmorProfile: Unconfined — these are required for rootlesskit to set up user and mount namespaces. The pod does not have host privileges.

Rooted mode (buildkit.rootless: false): buildkitd runs as root with privileged: true. This is required on nodes that cannot support user namespaces (e.g. EKS Bottlerocket with SELinux enforcing). It is less secure than rootless — a container escape from buildkitd would have full host access. Only use rooted mode when rootless is not supported by the cluster.


Recommendations

ConcernRecommendation
Untrusted repositoriesReview canette.yaml and Dockerfile before triggering a build
Network exfiltrationAdd a NetworkPolicy restricting egress from canette-build
Vulnerable imagesEnable scanning via security.scan.enabled: "true"; set security.scan.mandatory: true to block deployments
Shared build cacheUse separate canette installations for trusted and untrusted workloads
Build secretsNever pass sensitive secrets as build args — use runtime secrets instead

On this page