Security model¶
devcontainer-builder turns a git URL into a pushed container image, and the Coder template turns that image into a running workspace pod. Both steps handle credentials and both need some privilege. This page explains where the trust boundaries are, what each component is trusted to do, and what a platform admin is expected to add around it. It's background reading; the settings themselves are in the Helm chart and Coder template references.
The API is unauthenticated, so it stays inside the cluster¶
The HTTP API has no authentication of its own. Anyone who can reach
POST /build can make the service clone any repository it can reach and
push an image to any registry it has credentials for, using the service's
own server-side git and registry credentials when the request doesn't
bring its own. GET /logs/{id} returns captured build output, and
DELETE /image deletes images.
That is a deliberate scope decision, not an oversight: the service is platform infrastructure called by Coder's provisioner, in the same cluster. The chart reflects it:
- one
ClusterIPService, noIngress, noLoadBalancer— and none should be added; - one replica, called only by in-cluster clients.
Recommended: a NetworkPolicy. "Cluster-internal" still means every
pod in the cluster can reach the service. On a multi-tenant cluster,
including one where Coder workspaces themselves run, restrict ingress to
the Coder provisioner's namespace. The chart doesn't ship one, because the
right selector depends on where Coder runs. A minimal example:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: devcontainer-builder-ingress
namespace: devcontainer-builder
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: devcontainer-builder
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: coder
ports:
- port: 8080
Without a policy, a developer with a terminal in a workspace can call the build service directly. That gives them nothing their own git access doesn't — except the service's server-side credentials (see below), which is the main reason to scope them narrowly and to add the policy.
Credentials¶
Per-build token forwarding¶
The template prefers credentials that belong to the person creating the
workspace. With external_auth_id set, Coder asks the user to link their
git account, and the template forwards that user's token with the one
POST /build that builds their workspace. The service uses it for that
clone only: written to a scratch .netrc in a per-request $HOME, never
on a command line or in the clone URL, and deleted when the request ends.
See Credential handling and
ADR-0002.
A user can therefore only build repositories they can already read. The token is not persisted by the service.
Server-side credentials¶
The service can also hold deployment-wide credentials: registryAuth
for pushing, and gitCredentials per git host. These apply to every
request that doesn't bring its own, so anyone who can call the API can use
them. Keep them to what the platform needs:
- a registry token scoped to the one repository namespace images are pushed to;
- git credentials only for hosts where every caller should have the same read access (or none at all, relying on per-build tokens).
Template-wide git_credentials_username/git_credentials_token work the
same way: every workspace built from that template uses them.
What ends up in Terraform state¶
Coder keeps each workspace's Terraform state in its database. The
devcontainerbuilder_build resource stores its inputs there, including
git_credentials (the forwarded user token or the template-wide token)
and registry_credentials when set. They are marked sensitive, which
hides them from plan output, but state holds them in plain text. The
template ignores later changes to git_credentials so a refreshed token
doesn't rebuild the image, which means state keeps the token from the
last build until the next Rebuild.
Treat Coder's database and its backups as holding git tokens, and prefer short-lived tokens (Coder's external auth refreshes OAuth tokens) over long-lived personal access tokens in template variables.
BuildKit runs privileged¶
BuildKit's default (rootful) mode needs a privileged container to run OCI builds. That is inherent to BuildKit, not to devcontainer-builder, and it's why the build runs in a separate, long-lived BuildKit instance instead of Docker-in-Docker in the service pod (ADR-0001).
Consequences:
- The namespace BuildKit runs in needs the
privilegedPod Security level. With the chart's bundled BuildKit, that's the release namespace — install it in its own namespace, neverdefault(Quickstart). - A
RUNstep in any repository's Dockerfile executes inside BuildKit's build sandbox. Building untrusted repositories means trusting BuildKit's isolation; the service itself stays unprivileged and non-root. - BuildKit's own endpoint is unauthenticated plain TCP in the bundled
setup. Apply the same
NetworkPolicythinking to it: only the devcontainer-builder pod needs to reach it.
Workspace pods and allow_privileged¶
Workspace pods run as the repository's remoteUser (runAsUser from the
image), not root, and the template keeps them within Pod Security
baseline by default. A devcontainer.json asking for more —
privileged: true, securityOpt: seccomp=unconfined, or capabilities
outside baseline's list such as SYS_PTRACE — is not honored; the
build log and the dashboard show a warning instead.
The template variable allow_privileged turns those on. Only set it when:
- the workspace namespace's Pod Security level allows it (otherwise the pod is rejected); and
- you accept that any repository a user points a workspace at can then get a privileged pod, which is effectively root on the node.
A safer pattern is a separate template, with allow_privileged = true and
its own namespace, offered only to the people who need it.
Pod Security levels, summarized¶
| Namespace | Runs | Pod Security level |
|---|---|---|
| devcontainer-builder (with bundled BuildKit) | service + BuildKit | privileged (BuildKit) |
| devcontainer-builder (external BuildKit) | service only | baseline |
| BuildKit's own namespace, if separate | BuildKit | privileged |
| Coder workspaces | workspace pods | baseline; privileged only with allow_privileged |
VS Code in the browser: Microsoft's license¶
VS Code in the browser (the VS Code Web button) runs Microsoft's VS
Code Server and installs extensions from the Microsoft Marketplace. Its
license permits use within your own
organization. The template variable accept_vscode_license (default
true) accepts it on your users' behalf. If you offer workspaces to people
outside your organization, set it to false: that removes the browser IDE,
and VS Code Desktop is unaffected. See
Coder template.
What this model doesn't cover¶
- Authentication and authorization of developers is Coder's job: who can create workspaces, from which templates.
- Image provenance and scanning. Images are built from whatever the repository says; scan them in your registry if that matters to you.
- Supply chain of Dev Container Features and base images, which are
fetched at build time from wherever
devcontainer.jsonpoints.