Stop build.py sweeping secrets and bytecode into gen/
gen/<room>/ is the docker build context and soleprint/Dockerfile is `COPY . .`,
so anything reaching gen/ reaches an image layer — and registry.mcrn.ar is
public-read. station/tools/tester/.env has been gitignored since the last
incident, but .gitignore does not bind shutil: copy_path() called
shutil.copytree() with no ignore=, so the key was copied into every built room.
Verified extractable from soleprint_localtest-soleprint:latest (built 8 days
ago) at /app/station/tools/tester/.env.
ctrl/deploy.sh's --exclude='.env' is why this looked handled; it only covers the
rsync path, not the build-and-push path.
Two layers now:
- copy_path()/merge_into() filter .env, __pycache__, *.pyc, .git, node_modules
and virtualenvs out of bulk directory copies. Single-file copies named by a
caller are untouched, so cfg/<room>/.env.example still ships.
- soleprint/.dockerignore repeats the rule at the docker boundary and is
copied into the context beside the Dockerfile. Follows the convention
soleprint/atlas/.dockerignore already set (.env, .env.*, !.env.example).
Runtime is unaffected: no Dockerfile COPYs a .env, and the room compose files
supply it with `env_file: - .env`, read from the host at run time.
The key itself still needs rotating — it remains in git history.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
build.py
29
build.py
@@ -80,6 +80,30 @@ def _rmtree_resilient(path: Path):
|
||||
)
|
||||
|
||||
|
||||
# Never swept into a built room, wherever they appear in a source tree.
|
||||
#
|
||||
# This is a SECURITY boundary, not tidiness. gen/<room>/ is the docker build
|
||||
# context, soleprint/Dockerfile is `COPY . .`, and there is no .dockerignore —
|
||||
# so anything that reaches gen/ reaches an image layer, and registry.mcrn.ar is
|
||||
# public-read. That is how station/tools/tester/.env, gitignored since the last
|
||||
# incident, still ended up baked into soleprint_localtest-soleprint:latest with
|
||||
# its API key intact. .gitignore does not bind shutil.
|
||||
#
|
||||
# Applied to bulk directory copies only. A caller naming a single file is making
|
||||
# an explicit request (cfg/<room>/.env.example is the one that matters) and is
|
||||
# left alone.
|
||||
ALWAYS_IGNORE = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env"}
|
||||
ALWAYS_IGNORE_SUFFIXES = (".pyc", ".pyo")
|
||||
|
||||
|
||||
def is_ignored(name: str) -> bool:
|
||||
return name in ALWAYS_IGNORE or name.endswith(ALWAYS_IGNORE_SUFFIXES)
|
||||
|
||||
|
||||
def _copytree_ignore(directory, files):
|
||||
return {f for f in files if is_ignored(f)}
|
||||
|
||||
|
||||
def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||
"""Copy file or directory, resolving symlinks."""
|
||||
if target.is_symlink():
|
||||
@@ -91,7 +115,7 @@ def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||
target.unlink()
|
||||
|
||||
if source.is_dir():
|
||||
shutil.copytree(source, target, symlinks=False)
|
||||
shutil.copytree(source, target, symlinks=False, ignore=_copytree_ignore)
|
||||
if not quiet:
|
||||
log.info(f" {target.name}/")
|
||||
else:
|
||||
@@ -111,6 +135,8 @@ def merge_into(source: Path, target: Path):
|
||||
for item in source.rglob("*"):
|
||||
if item.is_file():
|
||||
rel = item.relative_to(source)
|
||||
if any(is_ignored(part) for part in rel.parts):
|
||||
continue
|
||||
dest = target / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(item, dest)
|
||||
@@ -687,6 +713,7 @@ def build_soleprint(output_dir: Path, room: str):
|
||||
"index.html",
|
||||
"requirements.txt",
|
||||
"Dockerfile",
|
||||
".dockerignore",
|
||||
]:
|
||||
if (soleprint / name).exists():
|
||||
copy_path(soleprint / name, output_dir / name)
|
||||
|
||||
24
soleprint/.dockerignore
Normal file
24
soleprint/.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# The build context is gen/<room>/, and the Dockerfile is `COPY . .` — so this
|
||||
# file is the last thing standing between a stray secret and a public image
|
||||
# layer. build.py already filters these out of the copy into gen/; this repeats
|
||||
# the rule at the docker boundary so a hand-built context, or a future copy path
|
||||
# that forgets, still cannot bake one in.
|
||||
#
|
||||
# Copied into gen/<room>/ by build.py's named-file list alongside the Dockerfile.
|
||||
|
||||
.env
|
||||
.env.*
|
||||
**/.env
|
||||
**/.env.*
|
||||
!.env.example
|
||||
!**/.env.example
|
||||
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
.git/
|
||||
.venv/
|
||||
venv/
|
||||
node_modules/
|
||||
Reference in New Issue
Block a user