Harden the container: non-root, pinned images and dependencies, the key in a file (B-91, SEC-51, SEC-52, SEC-53; closes B-71, B-72)

- app runs as uid 1000; a one-shot data-permissions service chowns ./data to it
  and closes it to others (works on an older root-owned install with no host sudo);
  files are created 0600 (umask 077)
- python:3.12-slim and caddy:2-alpine pinned by digest; dependencies installed from
  requirements.lock with --require-hashes
- the Fernet key is XPRV_ENCRYPTION_KEY_FILE (./secrets/xprv_encryption.key, a compose
  secret), out of every environment variable and of a ./data backup
- setup.md no longer presents a second way to run the server (B-72)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-18 00:54:23 +02:00
1 parent 40ac490826
commit c1b192f6d1
23 files changed
+1500 -183

No files matched your search

+18 -1
View File
@@ -19,6 +19,14 @@ logger = logging.getLogger(__name__)
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
def model_post_init(self, __context) -> None:
if self.xprv_encryption_key_file and not self.xprv_encryption_key:
try:
with open(self.xprv_encryption_key_file) as f:
self.xprv_encryption_key = f.read().strip()
except OSError:
pass # validate_runtime_secrets reports it, naming the file
database_url: str = "sqlite+aiosqlite:///./plm_lottery.db"
electrum_host: str = "santantonio.sytes.net"
@@ -45,6 +53,10 @@ class Settings(BaseSettings):
start_paused: bool = False
xprv_encryption_key: str = ""
# B-91 (SEC-53): the key as a file (a compose secret in the container), so it is in no
# environment variable and `docker inspect` cannot show it. When set it is read into
# `xprv_encryption_key` at load, which is what the rest of the code uses.
xprv_encryption_key_file: str = ""
master_key_path: str = "./master.xprv.enc"
jwt_secret: str = ""
jwt_algorithm: str = "HS256"
@@ -95,8 +107,13 @@ def validate_runtime_secrets(config: Settings | None = None) -> None:
'(generate: python -c "import secrets; print(secrets.token_urlsafe(32))")'
)
if not config.xprv_encryption_key.strip():
where = (
f"XPRV_ENCRYPTION_KEY_FILE ({config.xprv_encryption_key_file}) is missing or empty"
if config.xprv_encryption_key_file
else "XPRV_ENCRYPTION_KEY_FILE must point to a file holding the key"
)
problems.append(
"XPRV_ENCRYPTION_KEY must be set (generate: python -c "
where + " (generate: python -c "
'"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")'
)
if problems: