Skip to content

Commit be3ac01

Browse files
fix(templates): do not mount the CodeArtifact secret while building project code
`Dockerfile-uv.j2` installs in two steps and mounted the broker secret on both. The second step runs `uv sync` after `COPY project`, which builds the agent's own project — and a PEP 517 backend is project-controlled code, either an in-tree module named by `backend-path` or whatever `[build-system] requires` pulls in. With the secret mounted there it can read the token, and the build has network. Reproduced before fixing. The vector needs only files the templates already copy — `pyproject.toml` declaring `backend-path = ["project"]`, plus one module under `project/` — so no Dockerfile change is required, which is exactly the threat PRIVATE_INDEX.md already says it defends against: a contributed change to a project file is far less conspicuous in review than a curl in a Dockerfile. What the backend captured: secret-file: index-url = https://aws:FAKE%2BTOKEN%2FVALUE%3D@.../simple/ env password: FAKE+TOKEN/VALUE= <- decoded, not just the encoded URL The second step no longer mounts it. Dependencies, private ones included, are already installed by the first step, so nothing is lost. Verified against a package published ONLY to a local private index and absent from public PyPI (pypi.org returns 404 for it), which is the sgp-obs case: | build succeeds | both steps: yes | step 1 only: yes | | private-index package installs | yes | YES | | backend reads the secret file | yes | <absent> | | backend reads the decoded token | yes | <unset> | `Dockerfile.j2`, the requirements.txt variant, never had this: it installs and only then copies the project, so no project-controlled code runs while the secret is mounted. Left alone. Raised by Greptile on #525. Fixing it here rather than there — #525 is the release-please branch, and a push to it would be overwritten by the next regeneration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent bc51c52 commit be3ac01

20 files changed

Lines changed: 222 additions & 152 deletions

File tree

‎src/agentex/lib/cli/templates/PRIVATE_INDEX.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,38 @@ The name must be exactly `scale-pypi`. uv applies `UV_INDEX_SCALE_PYPI_USERNAME`
4242
silently stop applying. Setting `UV_INDEX_URL` instead does not authenticate a *named* index at
4343
all, and the resolve fails with a 401.
4444

45+
## The secret is scoped to the dependency step only
46+
47+
`Dockerfile-uv.j2` installs in two steps, and only the first one mounts the broker secret:
48+
49+
| Step | What it does | Secret |
50+
| --- | --- | --- |
51+
| `uv sync --no-install-project` | resolves and installs dependencies, private ones included | mounted |
52+
| `uv sync` (after `COPY project`) | builds and installs the agent's own project | **not mounted** |
53+
54+
The second step runs the agent's own PEP 517 build backend, and that is project-controlled
55+
code — an in-tree `backend-path` module, or whatever `[build-system] requires` names. With the
56+
secret mounted there it can read `/run/secrets/codeartifact-pip-conf` and the decoded
57+
`UV_INDEX_SCALE_PYPI_PASSWORD`, and the build has network, so the CodeArtifact token can go
58+
anywhere. That is the same threat this file already describes below — a contributed change to a
59+
*project* file, far less conspicuous in review than a `curl` in the Dockerfile — and it needs
60+
only files the templates already copy: `pyproject.toml` and something under `project/`.
61+
62+
Reproduced against the previous layout, then re-run against this one:
63+
64+
| | secret on both steps | secret on step 1 only |
65+
| --- | --- | --- |
66+
| build succeeds | yes | yes |
67+
| package from the private index installs | yes | **yes** |
68+
| build backend reads the secret file | **yes** | `<absent>` |
69+
| build backend reads the decoded token | **yes** | `<unset>` |
70+
71+
Nothing is lost by dropping it: dependencies are already installed by the first step, so the
72+
second has nothing left to fetch from the mirror.
73+
74+
`Dockerfile.j2` (the `requirements.txt` variant) never had this problem — it installs and only
75+
then copies the project, so no project-controlled code runs while the secret is mounted.
76+
4577
## Three things that are easy to get wrong
4678

4779
**The token arrives percent-encoded.** The buildspec URL-encodes it to embed it in the pip config's

‎src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
5353
# Copy the project code
5454
COPY {{ project_path_from_build_root }}/project ./project
5555

56-
# Install the project
56+
# Install the project.
57+
#
58+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
59+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
60+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
61+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
62+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
63+
# network. Reproduced before this change. Dependencies — including the private ones —
64+
# are already installed by the step above, so nothing is lost by dropping it here.
65+
# See PRIVATE_INDEX.md.
5766
RUN --mount=type=cache,target=/root/.cache/uv \
58-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64-
fi; \
6567
uv sync --no-dev
6668

6769
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
5353
# Copy the project code
5454
COPY {{ project_path_from_build_root }}/project ./project
5555

56-
# Install the project
56+
# Install the project.
57+
#
58+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
59+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
60+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
61+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
62+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
63+
# network. Reproduced before this change. Dependencies — including the private ones —
64+
# are already installed by the step above, so nothing is lost by dropping it here.
65+
# See PRIVATE_INDEX.md.
5766
RUN --mount=type=cache,target=/root/.cache/uv \
58-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64-
fi; \
6567
uv sync --no-dev
6668

6769
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4949
# Copy the project code
5050
COPY {{ project_path_from_build_root }}/project ./project
5151

52-
# Install the project
52+
# Install the project.
53+
#
54+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
55+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
56+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
57+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
58+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
59+
# network. Reproduced before this change. Dependencies — including the private ones —
60+
# are already installed by the step above, so nothing is lost by dropping it here.
61+
# See PRIVATE_INDEX.md.
5362
RUN --mount=type=cache,target=/root/.cache/uv \
54-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60-
fi; \
6163
uv sync --no-dev
6264

6365
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4949
# Copy the project code
5050
COPY {{ project_path_from_build_root }}/project ./project
5151

52-
# Install the project
52+
# Install the project.
53+
#
54+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
55+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
56+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
57+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
58+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
59+
# network. Reproduced before this change. Dependencies — including the private ones —
60+
# are already installed by the step above, so nothing is lost by dropping it here.
61+
# See PRIVATE_INDEX.md.
5362
RUN --mount=type=cache,target=/root/.cache/uv \
54-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60-
fi; \
6163
uv sync --no-dev
6264

6365
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/default-pydantic-ai/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4949
# Copy the project code
5050
COPY {{ project_path_from_build_root }}/project ./project
5151

52-
# Install the project
52+
# Install the project.
53+
#
54+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
55+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
56+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
57+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
58+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
59+
# network. Reproduced before this change. Dependencies — including the private ones —
60+
# are already installed by the step above, so nothing is lost by dropping it here.
61+
# See PRIVATE_INDEX.md.
5362
RUN --mount=type=cache,target=/root/.cache/uv \
54-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60-
fi; \
6163
uv sync --no-dev
6264

6365
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/default/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4949
# Copy the project code
5050
COPY {{ project_path_from_build_root }}/project ./project
5151

52-
# Install the project
52+
# Install the project.
53+
#
54+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
55+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
56+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
57+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
58+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
59+
# network. Reproduced before this change. Dependencies — including the private ones —
60+
# are already installed by the step above, so nothing is lost by dropping it here.
61+
# See PRIVATE_INDEX.md.
5362
RUN --mount=type=cache,target=/root/.cache/uv \
54-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60-
fi; \
6163
uv sync --no-dev
6264

6365
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/sync-claude-code/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
5353
# Copy the project code
5454
COPY {{ project_path_from_build_root }}/project ./project
5555

56-
# Install the project
56+
# Install the project.
57+
#
58+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
59+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
60+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
61+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
62+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
63+
# network. Reproduced before this change. Dependencies — including the private ones —
64+
# are already installed by the step above, so nothing is lost by dropping it here.
65+
# See PRIVATE_INDEX.md.
5766
RUN --mount=type=cache,target=/root/.cache/uv \
58-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64-
fi; \
6567
uv sync --no-dev
6668

6769
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/sync-codex/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
5353
# Copy the project code
5454
COPY {{ project_path_from_build_root }}/project ./project
5555

56-
# Install the project
56+
# Install the project.
57+
#
58+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
59+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
60+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
61+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
62+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
63+
# network. Reproduced before this change. Dependencies — including the private ones —
64+
# are already installed by the step above, so nothing is lost by dropping it here.
65+
# See PRIVATE_INDEX.md.
5766
RUN --mount=type=cache,target=/root/.cache/uv \
58-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64-
fi; \
6567
uv sync --no-dev
6668

6769
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

‎src/agentex/lib/cli/templates/sync-langgraph/Dockerfile-uv.j2‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4949
# Copy the project code
5050
COPY {{ project_path_from_build_root }}/project ./project
5151

52-
# Install the project
52+
# Install the project.
53+
#
54+
# No broker secret on this step, deliberately. Unlike the step above, this one runs the
55+
# agent's OWN build backend — and a PEP 517 backend is project-controlled code (an
56+
# in-tree `backend-path` module, or whatever `[build-system] requires` names). With the
57+
# secret mounted here it could read /run/secrets/codeartifact-pip-conf and the decoded
58+
# UV_INDEX_SCALE_PYPI_PASSWORD and send the CodeArtifact token anywhere; the build has
59+
# network. Reproduced before this change. Dependencies — including the private ones —
60+
# are already installed by the step above, so nothing is lost by dropping it here.
61+
# See PRIVATE_INDEX.md.
5362
RUN --mount=type=cache,target=/root/.cache/uv \
54-
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55-
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56-
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57-
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58-
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59-
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60-
fi; \
6163
uv sync --no-dev
6264

6365
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

0 commit comments

Comments
 (0)