Tasks: Secure Container Deployment with Credential Isolation (US3)¶
Input: Design documents from /docs/product-specs/024-claude-serve-deploy/
Prerequisites: plan.md (required), spec.md (required), research.md, data-model.md, contracts/health-endpoint.md
Tests: TDD approach — write tests FIRST, verify they FAIL, then implement.
Organization: Tasks are grouped by phase. US3 focuses on Dockerfile security hardening (non-root user, capability drops, read-only root filesystem, tmpfs support) and environment variable pass-through for the proxy credential injection pattern (ANTHROPIC_BASE_URL).
Format: [ID] [P?] [US3] Description¶
- [P]: Can run in parallel (different files, no dependencies)
- [US3]: All user story tasks belong to User Story 3
- Include exact file paths in descriptions
Path Conventions¶
- Single project:
src/holodeck/,tests/at repository root - Docker assets:
docker/Dockerfile,docker/entrypoint.sh - Deploy module:
src/holodeck/deploy/dockerfile.py
Phase 1: Setup — Understand Current State¶
Purpose: Audit the existing Dockerfile, entrypoint, and deploy template to identify gaps against Anthropic secure deployment guidelines.
- T001 Audit
docker/Dockerfilefor existing security posture: verify non-root user (holodeck, uid 1000) is already created,/appownership is set,USER holodeckis the final layer. Document any gaps (missing tmpfs-compatible directory structure, missing/tmppermissions, no capability-awareness notes). - T002 Audit
docker/entrypoint.shfor non-root compatibility: verify all file operations use paths owned byholodeckuser, noapt-getor root-only operations at runtime, no hardcoded absolute paths outside/appand/tmp. Document the--configbug on line 111 (uses--configflag instead of positional argument). - T003 Audit
src/holodeck/deploy/dockerfile.pyJinja2 template (HOLODECK_DOCKERFILE_TEMPLATE) for security: verify non-root user switch happens after allCOPY/RUNoperations,chowncovers all copied files, no writable paths outside/appare assumed.
Checkpoint: Current security posture documented — gaps identified for US3 implementation.
Phase 2: Foundational — US1/US2 Dependency Verification¶
Purpose: Verify that US1 (serve infrastructure) and US2 (base Dockerfile with Node.js) provide the foundation US3 builds upon.
- T004 Verify US2 has added conditional Node.js installation block (
{% if needs_nodejs %}) tosrc/holodeck/deploy/dockerfile.pytemplate. US3 security hardening layers build on top of this Node.js block. - T005 Verify US2 has added Node.js installation to
docker/Dockerfilebase image. US3 non-root and tmpfs changes must not break the Node.js layer or prevent thenodebinary from being executable by theholodeckuser. - T006 Verify US1 serve infrastructure passes environment variables through to
ClaudeBackend. Confirm thatbuild_options()insrc/holodeck/lib/backends/claude_backend.py(line 272) constructsenvdict by merging auth + otel vars but does NOT replaceos.environ— inherited env vars likeANTHROPIC_BASE_URLmust reach the SDK subprocess.
Checkpoint: US1/US2 foundations confirmed — US3 implementation can proceed safely.
Phase 3: User Story 3 — Secure Container Deployment¶
Purpose: Implement and test all acceptance scenarios for secure container deployment with credential isolation.
3a. Non-Root User and Directory Permissions¶
- T007 [P] [US3] Write unit test
test_dockerfile_template_non_root_user_final_layerintests/unit/deploy/test_dockerfile.py— generate a Dockerfile viagenerate_dockerfile()withneeds_nodejs=True, assert the lastUSERdirective isUSER holodeck(not root), and that it appears after allCOPYandRUNcommands. Verify the test FAILS if the USER directive is removed. - T008 [P] [US3] Write unit test
test_dockerfile_template_chown_covers_all_copied_pathsintests/unit/deploy/test_dockerfile.py— generate a Dockerfile withinstruction_files=["instructions.md"]anddata_directories=["data/"], assertchown -R holodeck:holodeck /appappears after all COPY operations, ensuring all files are owned by the non-root user. - T009 [P] [US3] Write unit test
test_dockerfile_template_tmpfs_compatible_dirsintests/unit/deploy/test_dockerfile.py— generate a Dockerfile withneeds_nodejs=True, assert the template creates a/tmpdirectory with holodeck ownership OR does not write to any path outside/appand/tmp. Verify the container can function when/is read-only and only/tmpis writable. - T010 [P] [US3] Modify
HOLODECK_DOCKERFILE_TEMPLATEinsrc/holodeck/deploy/dockerfile.pyto add a tmpfs-compatible working directory block: after theWORKDIR /appline, addRUN mkdir -p /tmp/holodeck && chown holodeck:holodeck /tmp/holodeckso the Claude SDK subprocess has a writable scratch space when root filesystem is read-only. AddENV TMPDIR=/tmp/holodeckto direct temp file operations to the owned directory. - T011 [P] [US3] Modify
docker/Dockerfilebase image to add tmpfs-compatible directory: after theWORKDIR /appline and beforeUSER holodeck, addRUN mkdir -p /tmp/holodeck && chown holodeck:holodeck /tmp/holodeck. SetENV TMPDIR=/tmp/holodeck.
3b. Capability Drop Compatibility¶
- T012 [P] [US3] Write unit test
test_entrypoint_no_privileged_operationsintests/unit/deploy/test_entrypoint.py(new file) — parsedocker/entrypoint.shas text, assert it contains noapt-get,yum,apk,mount,chmod(except on/app/entrypoint.shduring build),chown, or other operations requiring elevated capabilities. This ensures the entrypoint works with--cap-drop ALL. - T013 [P] [US3] Write unit test
test_dockerfile_no_runtime_root_operationsintests/unit/deploy/test_dockerfile.py— generate a Dockerfile viagenerate_dockerfile(), parse the output, assert that noRUNcommands appear after the finalUSER holodeckdirective. All root operations must complete before the user switch. - T014 [P] [US3] Write integration test
test_generated_dockerfile_cap_drop_compatibleintests/unit/deploy/test_dockerfile.py— generate a full Dockerfile withneeds_nodejs=True, parse it line by line, build a list of operations that occur afterUSER holodeck, assert none of them require root capabilities (noapt-get,pip install --system,chmodon system paths, etc.).
3c. Read-Only Root Filesystem Support¶
- T015 [P] [US3] Write unit test
test_dockerfile_template_no_writable_root_assumptionsintests/unit/deploy/test_dockerfile.py— generate a Dockerfile, scan allENV,RUN, andCOPYdirectives, assert no environment variable points to a path outside/app,/tmp, or/home/holodeckas a writable location. This validates read-only root filesystem compatibility. - T016 [P] [US3] Write unit test
test_entrypoint_uses_only_tmpfs_safe_pathsintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, extract all file paths referenced (via regex for absolute paths), assert all writable paths are under/app,/tmp, or/home/holodeck. No writes to/var,/etc,/usr, etc. - T017 [US3] Modify
docker/entrypoint.shto setTMPDIR=/tmp/holodeckif not already set, ensuring Claude SDK subprocess temp files go to the tmpfs-mounted directory. Add after the default configuration block (line 19):HOLODECK_TMPDIR="${TMPDIR:-/tmp/holodeck}"andexport TMPDIR="${HOLODECK_TMPDIR}".
3d. Environment Variable Pass-Through (Proxy Pattern)¶
- T018 [P] [US3] Write unit test
test_env_passthrough_anthropic_base_urlintests/unit/lib/backends/test_claude_backend.py— mockos.environto includeANTHROPIC_BASE_URL=http://localhost:8081, call the standalonebuild_options()function fromholodeck.lib.backends.claude_backend(module-level function at line 237 ofclaude_backend.py, not a method onClaudeBackend) with a mockedAgentconfig, assert the resultingClaudeAgentOptions.enveither containsANTHROPIC_BASE_URLor (by design) does not filter it from the inherited environment. The key assertion: the SDK subprocess must receive this variable. - T019 [P] [US3] Write unit test
test_env_passthrough_http_proxy_varsintests/unit/lib/backends/test_claude_backend.py— mockos.environto includeHTTP_PROXY=http://proxy:3128andHTTPS_PROXY=http://proxy:3128, call the standalonebuild_options()function fromholodeck.lib.backends.claude_backend(module-level function at line 237 ofclaude_backend.py, not a method onClaudeBackend) with a mockedAgentconfig, assert proxy variables are not stripped or overridden by theenvdict construction atclaude_backend.pyline 272. Verify theenvdict merges with (not replaces) inherited environment. - T020 [P] [US3] Write unit test
test_env_passthrough_anthropic_api_keyintests/unit/lib/backends/test_claude_backend.py— mockos.environto includeANTHROPIC_API_KEY=sk-test-key, call the standalonebuild_options()function fromholodeck.lib.backends.claude_backend(module-level function at line 237 ofclaude_backend.py, not a method onClaudeBackend) with a mockedAgentconfig, assert the API key reaches the subprocess environment (either via explicitenvinclusion or inherited passthrough). - T021 [US3] If the standalone
build_options()function insrc/holodeck/lib/backends/claude_backend.py(module-level function at line 237, not a method onClaudeBackend) constructs theenvdict in a way that replaces (not merges with) the inherited environment, fix it. Theenvkwarg onClaudeAgentOptionsmust be additive — it adds/overrides specific keys but does not prevent other inherited env vars from reaching the subprocess. Verify by reading Claude Agent SDK docs forClaudeAgentOptions.envbehavior. - T022 [P] [US3] Write unit test
test_dockerfile_env_passthrough_vars_declaredintests/unit/deploy/test_dockerfile.py— generate a Dockerfile withneeds_nodejs=True, assert it does NOT hardcodeANTHROPIC_BASE_URL,ANTHROPIC_API_KEY,HTTP_PROXY, orHTTPS_PROXYto any value. These must be injected atdocker runtime, not baked into the image.
3e. Entrypoint Fixes for Non-Root Execution¶
- T023 [P] [US3] Write unit test
test_entrypoint_positional_arg_not_config_flagintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, extract theholodeck serveinvocation line, assert it uses positional argument format (holodeck serve ${HOLODECK_AGENT_CONFIG}) NOT--configflag format (holodeck serve --config ${HOLODECK_AGENT_CONFIG}). This validates the FR-011 fix. - T024 [US3] Fix
docker/entrypoint.shline 111: changeCMD_ARGS="--config ${HOLODECK_AGENT_CONFIG}"to build the command with the config as a positional argument. The corrected invocation should be:exec holodeck serve "${HOLODECK_AGENT_CONFIG}" --port "${HOLODECK_PORT}" --protocol "${HOLODECK_PROTOCOL}". Remove theCMD_ARGSvariable pattern in favor of direct argument passing for clarity. - T025 [P] [US3] Write unit test
test_entrypoint_exec_replaces_shellintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, assert theholodeck serveinvocation usesexecprefix, ensuring the shell process is replaced and the Python process receives signals directly (critical for non-root graceful shutdown with--cap-drop ALL).
3f. Claude-Specific Entrypoint Validation¶
- T026 [P] [US3] Write unit test
test_entrypoint_claude_validation_function_existsintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, assert avalidate_claude_requirementsfunction (or equivalent) is defined that checks fornodebinary availability andANTHROPIC_API_KEY(orANTHROPIC_BASE_URL) presence. - T027 [US3] Add
validate_claude_requirements()function todocker/entrypoint.sh— check if the agent config YAML containsprovider: anthropic(usinggrep), and if so: (1) verifynode --versionsucceeds, (2) verifyANTHROPIC_API_KEYorANTHROPIC_BASE_URLis set. On failure, calllog_errorwith actionable message andexit 1. Call this function frommain()aftervalidate_configand before launching serve. - T028 [P] [US3] Write unit test
test_entrypoint_claude_validation_checks_nodeintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, assert thevalidate_claude_requirementsfunction body contains a check fornode(e.g.,command -v nodeorwhich nodeornode --version). - T029 [P] [US3] Write unit test
test_entrypoint_claude_validation_checks_credentialsintests/unit/deploy/test_entrypoint.py— parsedocker/entrypoint.sh, assert thevalidate_claude_requirementsfunction body checks forANTHROPIC_API_KEYorANTHROPIC_BASE_URLenvironment variable presence.
3g. Inline Entrypoint Security (deploy build output)¶
- T030 [P] [US3] Write unit test
test_inline_entrypoint_non_root_compatibleintests/unit/cli/commands/test_deploy.py(following existing CLI command test naming pattern intests/unit/cli/commands/) — locate the inline entrypoint string insrc/holodeck/cli/commands/deploy.py_prepare_build_context(), assert it contains no root-only operations (apt-get,chmodon system paths, etc.) and usesexecfor theholodeck serveinvocation. - T031 [P] [US3] Write unit test
test_inline_entrypoint_claude_validationintests/unit/cli/commands/test_deploy.py(following existing CLI command test naming pattern intests/unit/cli/commands/) — for a Claude agent config (provider: anthropic), verify the inline entrypoint generated by_prepare_build_context()includes Node.js validation (node --versioncheck) before launchingholodeck serve. - T032 [US3] Modify the inline entrypoint in
src/holodeck/cli/commands/deploy.py_prepare_build_context()to add Claude-specific validation when the agent provider is anthropic: insert anode --versioncheck andANTHROPIC_API_KEY/ANTHROPIC_BASE_URLcheck before theexec holodeck serveline.
Checkpoint: All acceptance scenarios have corresponding tests and implementations.
Phase 4: Polish — Code Quality and Regression Verification¶
Purpose: Ensure all changes pass code quality checks and introduce no regressions.
- T033 [US3] Run
make formatto format all new and modified files with Black + Ruff. - T034 [US3] Run
make lintand fix any Ruff + Bandit violations in US3 files:src/holodeck/deploy/dockerfile.py,docker/entrypoint.sh,src/holodeck/cli/commands/deploy.py, and all new test files. - T035 [US3] Run
make type-checkand fix any MyPy errors in US3 modified Python files. - T036 [US3] Run
pytest tests/unit/deploy/ -n auto -vto verify all US3 unit tests pass. - T037 [US3] Run
pytest tests/unit/lib/backends/test_claude_backend.py -n auto -v -k "env_passthrough"to verify all environment pass-through tests pass. - T038 [US3] Run
make testto verify no regressions across the full test suite (FR-009).
Checkpoint: US3 complete — secure container deployment validated, code quality verified, no regressions.
Dependencies & Execution Order¶
External Dependencies (BLOCKING)¶
- US1 (Serve Infrastructure): Must be complete before US3. US3 depends on serve pre-flight validation infrastructure and
build_options()env handling. - US2 (Container Build with Node.js): Must be complete before US3. US3 security hardening layers on top of the Node.js Dockerfile additions from US2.
Phase Dependencies¶
- Phase 1 (Setup): No dependencies — audit tasks can start immediately
- Phase 2 (Foundational): Depends on US1 + US2 completion
- Phase 3 (US3 Tasks): Depends on Phase 2 verification
- Phase 4 (Polish): Depends on Phase 3 completion
Within Phase 3¶
- 3a (Non-Root User), 3b (Capability Drop), 3c (Read-Only FS), 3d (Env Pass-Through) can run in parallel — they modify different files
- 3e (Entrypoint Fixes) can run in parallel with 3a-3d — modifies
docker/entrypoint.sh(shared with 3c T017, sequence T017 before T024) - 3f (Claude Validation) depends on 3e (entrypoint structure must be fixed first)
- 3g (Inline Entrypoint) can run in parallel with 3e/3f — modifies
deploy.pynotentrypoint.sh
Parallel Execution Examples¶
Example 1: Phase 3a + 3b + 3c + 3d in parallel¶
Worker 1 (3a): T007, T008, T009, T010, T011 — Dockerfile non-root + tmpfs dirs
Worker 2 (3b): T012, T013, T014 — capability drop compatibility tests
Worker 3 (3c): T015, T016, T017 — read-only FS tests + TMPDIR fix
Worker 4 (3d): T018, T019, T020, T021, T022 — env var pass-through
Example 2: Phase 3e + 3g in parallel¶
Worker 1 (3e): T023, T024, T025 — entrypoint positional arg fix
Worker 2 (3g): T030, T031, T032 — inline entrypoint security
Example 3: All [P] tests from Phase 3¶
# Run all parallelizable US3 tests:
pytest tests/unit/deploy/test_dockerfile.py \
tests/unit/deploy/test_entrypoint.py \
tests/unit/cli/commands/test_deploy.py \
tests/unit/lib/backends/test_claude_backend.py \
-n auto -v -k "non_root or cap_drop or tmpfs or env_passthrough or positional_arg or read_only"
Implementation Strategy¶
Defense-in-Depth Security Layers¶
US3 implements Anthropic's secure deployment guidelines through four complementary layers:
-
Non-root execution (3a, 3e): The container runs as
holodeck(uid 1000). All files arechowned before theUSERswitch. The entrypoint usesexecfor proper signal handling. -
Capability drop compatibility (3b): No runtime operations require elevated Linux capabilities. All privileged operations (
apt-get,chown,chmod) happen duringdocker build, not atdocker runtime. -
Read-only root filesystem (3c): Writable paths are limited to
/tmp/holodeck(tmpfs-mounted at runtime) and/app(if not read-only). TheTMPDIRenv var directs all temp file creation to the tmpfs path. -
Credential isolation (3d, 3f): API keys and proxy URLs are injected at runtime via
docker run -e, never baked into the image. TheANTHROPIC_BASE_URLenv var enables the proxy pattern where a local credential-injecting proxy handles authentication.
Key Files Modified¶
| File | Phase | Changes |
|---|---|---|
src/holodeck/deploy/dockerfile.py |
3a | Add tmpfs-compatible dir, TMPDIR env var |
docker/Dockerfile |
3a | Add tmpfs-compatible dir, TMPDIR env var |
docker/entrypoint.sh |
3c, 3e, 3f | Fix positional arg bug, add TMPDIR, add Claude validation |
src/holodeck/cli/commands/deploy.py |
3g | Add Claude validation to inline entrypoint |
tests/unit/deploy/test_dockerfile.py |
3a, 3b, 3c, 3d | Add security-focused Dockerfile tests |
tests/unit/deploy/test_entrypoint.py |
3b, 3c, 3e, 3f | NEW: Entrypoint security and fix tests |
tests/unit/cli/commands/test_deploy.py |
3g | Add inline entrypoint security tests |
tests/unit/lib/backends/test_claude_backend.py |
3d | Add env var pass-through tests |
Acceptance Scenario Traceability¶
| Acceptance Scenario | Validating Tasks |
|---|---|
AS-1: ANTHROPIC_BASE_URL routes through proxy |
T018, T021, T022 |
AS-2: --cap-drop ALL --security-opt no-new-privileges works |
T012, T013, T014, T025 |
AS-3: --read-only --tmpfs /tmp works |
T009, T010, T011, T015, T016, T017 |
AS-4: --user 1000:1000 non-root works |
T007, T008, T023, T024, T030 |
FR Traceability¶
| Requirement | Validating Tasks |
|---|---|
| FR-003: Secure Dockerfile (non-root, capability-aware, read-only FS) | T007-T017 |
| FR-006: Env var pass-through to Claude SDK subprocess | T018-T022 |
| FR-011: Entrypoint positional arg fix | T023, T024 |
Notes¶
- [P] tasks = different files or independent test functions, no dependencies
- [US3] label maps all tasks to User Story 3 for traceability
- Entrypoint tests parse shell scripts as text (not execute them) for unit-level verification
- Environment pass-through tests mock
os.environto verify subprocess env construction - The
--configbug fix (T024) is a prerequisite for any container deployment — it must be verified before US3 is considered complete - The
TMPDIR=/tmp/holodeckpattern ensures Claude SDK subprocess temp files land in tmpfs without requiring the SDK to be aware of the deployment topology - Commit after each sub-phase (3a, 3b, etc.) or logical group of tasks
- Stop at any checkpoint to validate progress independently