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:
1 parent
40ac490826
commit
c1b192f6d1
23 files changed
+1500
-183
No files matched your search
+18
-1
@@ -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:
|
||||
|
||||
@@ -115,7 +115,7 @@ def verify_master_key() -> None:
|
||||
except Exception as exc:
|
||||
raise ConfigError(
|
||||
f"the master key at {settings.master_key_path} cannot be loaded ({type(exc).__name__}): "
|
||||
"check MASTER_KEY_PATH and XPRV_ENCRYPTION_KEY"
|
||||
"check MASTER_KEY_PATH and XPRV_ENCRYPTION_KEY_FILE"
|
||||
) from exc
|
||||
problems = sample_problems(account, load_sample(), settings.xprv_encryption_key)
|
||||
if problems:
|
||||
|
||||
Reference in new issue
Block a user