A security release for the 1.8 line, remediating an independent third-party
review of Terrapod that reported 49 findings.
The review was carried out by karl0r, who reported everything privately and
gave us time to fix it. Each finding has a GitHub security advisory crediting them as
reporter; those are published as one event once the last release in this
remediation programme ships, so that no advisory names a fix an operator cannot
yet take. The great majority of the findings are
fixed here; where a fix would have changed what a running 1.8 deployment does, it
is called out below rather than shipped quietly, and where it could not be made
drop-in it is named as deferred to 1.9.0.
Security
The fixes in this release, grouped by what an attacker could do before them.
Runs and state
- A plan-only run could write a workspace's state. The upload endpoint had no
phase, plan-only or lineage guard, so a run that was never allowed to change
anything could replace the state it had only been given to read — and a foreign
state at serial+1 was accepted outright. Uploads now carry the guard, and the
lineage check no longer treats an absent lineage as a reason to skip itself. - Runner-reported OPA, scan and plan-JSON results were trusted as given. What
a policy set is — mandatory or advisory, which policies it carries — is now
read from the database rather than from the runner's own report, so a
compromised runner cannot downgrade the gate that governs it. - A reduced plan now says it was reduced.
GHSA-v677-9r29-3xq8is that a
mandatory AI policy gate can be bypassed by padding the plan past the size at
which Terrapod reduces what it shows the model. Half of that is fixed here: the
reduction is now declared rather than silent, and a destroy is never traded away
for a create when the plan is trimmed. The gate refusing to rule on a plan it
could not see in full is 1.9.0, because it changes when a run blocks. So on
this line the padding is visible, not prevented — the advisory stays open. - The runner token outlived its phase. Called out as a limitation rather than
fixed on this line: see Deferred to 1.9.0.
Credentials
- A cloned-repo token is scoped to reading contents. A
git_http_authof type
vcs_connectionminted a token carrying every permission the GitHub App holds,
across every repository in the installation, and handed it to a runner Job. It
is now narrowed at the two call sites where a minted token leaves the API
process, not merely at the default they share. - A Vault dynamic value source could reach Vault's own auth and
sys
self-service paths, including Terrapod's own token. Those paths are refused. - A
vcs_connectionis no longer attachable by anyone who can name one. A
connection covers every repository its credential reaches and its id is
serialised to anyone with read on a workspace using it, so the id is
discoverable by design — naming one was therefore a grant. Creating or
repointing a workspace, and the three registry-module paths that accept a
connection, now require a claim to it. Platform admins are unaffected. If this
blocks a workflow you rely on,vcs.require_connection_authorization: false
restores the previous behaviour; it is on by default because the previous
behaviour is the finding.
Web
nextraised to 16.3.8, clearing a critical RCE advisory
(GHSA-p498-v437-472g) andGHSA-vcvr-r3jv-pc5jin the published
terrapod-webimage.- Stored XSS in the 3D graph tooltips, which rendered untrusted node names as
HTML. Node names come from state, so the payload needed only to be a resource
name. - The login redirect parameter was written to
window.locationunvalidated. - AI output could fetch a remote image, which is an exfiltration channel for
whatever the model was shown. - A Content-Security-Policy subset the application can actually satisfy is now
sent. It is a subset deliberately: a full CSP is not attempted here, and this is
defence in depth rather than the fix for any one finding.
SAML — assertion destination and recipient checks, request binding, replay
rejection and signature requirements. The five new checks default to permissive
on this line, because switching them on can stop an existing IdP integration
working. One implementation, two sets of defaults: strict on main/2.0, opt-in
here. Turn them on deliberately after checking your IdP against
docs/sso.md.
Storage — a presigned upload could choose what the subsequent download
served, by setting a content type the download then echoed.
Audit — Slack approve and discard left no audit record. They do now.
Tooling that could not report anything. Two findings were not a missing
control but a check that passed while measuring nothing, and both had been green
for as long as they had existed: the CI secret scan ran gitleaks with zero
detection rules, and the DAST templates asserted the healthy response, so
they fired on every correctly protected endpoint and were silent on a bypassable
one. Both are fixed and both now have guards, because each fix is one line that
reads as housekeeping.
terrapod-migrate — a TFE access tag no longer becomes a fleet-wide read
grant when migrating.
Three documentation commits correct claims the code does not support, and two
record behaviour that is deliberate and bounded rather than changed (the bootstrap
join token's missing expiry, and how an IdP group becomes a role).
Still true from 1.8.0, and worth repeating in a security release
A lagging runner silently evaluates a shared-evaluation policy set without its
data files. shared_evaluation and support_files are additive, so a runner
image older than 1.8.0 ignores both and evaluates one policy at a time. A rule
that calls a helper from a support file then fails to compile and the set reports
errored, which is visible. But a rule that reads data.<key> compiles fine,
the data is absent, the rule never matches — and a mandatory gate reports a
clean pass it has not earned. Upgrade runners before enabling shared_evaluation
on any set. Sets that leave it off are unaffected on any runner version.
Breaking Changes
-
A run that pushed to a repository using a VCS-connection credential now
fails with 403. The credential handed to a runner Job is narrowed to
contents: read, which is the least that can still clone, so anything the run
did beyond cloning — pushing a commit, opening a pull request — no longer works
with it. That is the finding rather than a side effect: the old token carried
every permission the GitHub App held across every repository in the
installation. If a run genuinely needs to push, give it a static credential
scoped to what it should reach. -
A GitLab
vcs_connectiongit credential now errors the run until a key is
set. A GitLab access token cannot be attenuated — there is no narrower form to
mint — so delivering one to a runner hands the runner everything that token can
do. There is no safe default for a credential that cannot be narrowed, so it is
off, and a run that relied on it fails loudly rather than silently continuing to
over-share. Set the credential explicitly to opt back in.
Fork pull requests — read this before assuming you are protected
GHSA-gp5w-76rw-c452 (critical): a pull request from a fork, or from an
untrusted author, triggered a run with the workspace's credentials.
This release adds the control — a per-workspace allow_fork_pr_plans setting,
across the API, go-terrapod, the provider, MCP and the UI — and defaults it
ON, which means a 1.8.2 deployment is not protected until you turn it off.
That is deliberate: defaulting it off would stop fork plans that work today on a
supported line, and a patch release does not remove working behaviour. 2.0
defaults it off.
So the action is yours: for any workspace whose repository takes pull requests
from people who should not be able to reach its credentials, set
allow_fork_pr_plans to false. docs/runbooks.md
has the curl and the provider attribute.
Correction to the docs inside this release. Two reference pages shipped here
state the opposite default and were found after the tag was cut:
docs/api-reference.md's workspace attribute table gives allow-fork-pr-plans as
false, and docs/vcs-integration.md opens its fork section with "defaults to
false". Both are wrong for 1.8.2 — it defaults true — and
docs/vcs-integration.md contradicts itself, saying "on by default on this release
line" further down. llms.txt and the UI are correct. The fix is on
release/v1.8 and ships with the next patch; this note is authoritative in the
meantime.
The UI now says so in every language. Writing these notes turned up that 26 of
the 33 locales stated 2.0's default — "off by default" — in the primary text beside
the toggle, and that every locale's autodiscovery hint contradicted itself ("on by
default: a fork pull request gets no speculative plan"). Both are corrected here.
An operator told the setting is already off would reasonably have concluded there
was nothing to do.
Deferred to 1.9.0
Named here so the gaps are not discovered by reading a diff. Each would change
what an existing deployment does, which is why it is not in a patch:
- the engine-environment allowlist (
GHSA-658f-j48w-w8m9), which could break a
bespokeapi.extraEnv; - a mandatory AI policy gate refusing to rule on a plan it could not see in full
(GHSA-v677-9r29-3xq8), which changes when a run blocks; terrapod mergeremoved, and PR-comment commands requiring push permission
(GHSA-x4jp-5g4j-f8rr, critical — comment commands do not check the commenter's
permission on this line);- a scoped
service_boundtoken being able to mint a wider one; - a listener no longer re-joining across pools (a name collision 409s instead of
silently moving the listener); - Helm
| default truehonouring an explicitly configuredfalse— which would
change DB-pool and SMTP behaviour for an existingvalues.yaml, including
possibly stopping SMTP using TLS; - label-based authorization for VCS connections, which needs a schema change the
current one does not.
Four of the 49 findings are not fixed because, on investigation, they did not
hold: an SSRF guard that resolves a host before judging it (so the unicode
spellings reported were already refused — there is a regression test for it now), a
status endpoint that does not return the peer address its report named, an
unauthenticated schema route on a repository that is public and ships its own route
contract, and a metadata address already denied on the port it uses. Those four
advisories stay unpublished rather than being published and then argued with.
Upgrading
Drop-in except for the GitLab credential above. One schema migration adds the
fork-PR column; it is additive, has a server default, and is sequenced so that a
1.8 deployment upgrading to 1.9.0 or 2.0 meets it exactly once.
1.7 reaches end of life with 1.9.0. v1.7.7 ships alongside this release with
the same fixes except the fork-PR control, which could not be sequenced onto both
lines. If you are on 1.7 and your workspaces serve repositories that take
outside pull requests, upgrading to 1.8.2 is the remedy.
Status
Stable.
Verification
All five images are published multi-arch at :v1.8.2, the Helm chart is 1.8.2
with appVersion: v1.8.2, and 44 assets are attached. Every pipeline gate passed,
including Eval Boot on both kind and k3d — those two were cancelled by an
operator error while the pipeline was finishing (after every build, manifest and
release job had already succeeded) and were re-run to completion, so the tag's run
is green end to end.
Full Changelog: v1.8.1...v1.8.2