Remove hardcoded credentials, harden deployment, optimize OCR
Secrets (S3 keys, PG password, DeerMapper API key) were committed in config.yaml and .env and remain in git history. This removes them from the tracked tree and moves all secrets to env injection. Security: - config.yaml: drop all credentials, keep only non-secret app tunables - untrack .env, add .env.example template; .gitignore excludes .env - main.py: tolerant config lookups + fail-fast validation for missing secrets - docker-compose: env_file injection, no full-repo bind mount, debug port off - Dockerfile: bake config into image, run as non-root user Efficiency: - OCR: run the second (expensive) tesseract pass only when the first is unparsable; identical fallback behavior Docs: - README with operation + security notes - MIGRATION.md runbook: secret rotation, server cutover, decommission, git history purge Note: the leaked secrets are compromised and MUST be rotated; removing them from the tree is not sufficient. See MIGRATION.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
60
main.py
60
main.py
@@ -101,22 +101,22 @@ def load_config(path: str) -> AppConfig:
|
||||
return value.strip().lower() in ("1", "true", "yes", "on")
|
||||
return bool(value)
|
||||
|
||||
s3 = raw["s3"]
|
||||
pg = raw["postgres"]
|
||||
app = raw.get("app", {})
|
||||
deermapper = raw.get("deermapper-api", raw.get("deermapper_api", {}))
|
||||
s3 = raw.get("s3") or {}
|
||||
pg = raw.get("postgres") or {}
|
||||
app = raw.get("app") or {}
|
||||
deermapper = raw.get("deermapper-api") or raw.get("deermapper_api") or {}
|
||||
default_workers = max(1, min(16, (os.cpu_count() or 2) * 2))
|
||||
parallel_workers = int(app.get("parallel_workers", default_workers))
|
||||
parallel_workers = max(1, parallel_workers)
|
||||
s3_pool = int(app.get("s3_max_pool_connections", max(16, parallel_workers * 4)))
|
||||
s3_pool = max(10, s3_pool)
|
||||
|
||||
return AppConfig(
|
||||
s3_endpoint=env_or("S3_ENDPOINT", s3["endpoint"]),
|
||||
s3_access_key=env_or("S3_ACCESS_KEY", s3["access_key"]),
|
||||
s3_secret_key=env_or("S3_SECRET_KEY", s3["secret_key"]),
|
||||
s3_bucket=env_or("S3_BUCKET", s3["bucket"]),
|
||||
pg_dsn=env_or("PG_DSN", pg["dsn"]),
|
||||
cfg = AppConfig(
|
||||
s3_endpoint=env_or("S3_ENDPOINT", s3.get("endpoint", "")),
|
||||
s3_access_key=env_or("S3_ACCESS_KEY", s3.get("access_key", "")),
|
||||
s3_secret_key=env_or("S3_SECRET_KEY", s3.get("secret_key", "")),
|
||||
s3_bucket=env_or("S3_BUCKET", s3.get("bucket", "")),
|
||||
pg_dsn=env_or("PG_DSN", pg.get("dsn", "")),
|
||||
entrance_prefix=app.get("entrance_prefix", "icu/entrance/"),
|
||||
processed_prefix=app.get("processed_prefix", "icu/processed/"),
|
||||
thumb_prefix=app.get("thumb_prefix", "icu/thumbnails/"),
|
||||
@@ -138,6 +138,29 @@ def load_config(path: str) -> AppConfig:
|
||||
deermapper_api_image_field=app.get("deermapper_api_image_field", "image"),
|
||||
)
|
||||
|
||||
# Fail-fast: Secrets kommen ausschliesslich aus der Umgebung (.env). Fehlen sie,
|
||||
# bricht der Start mit klarer Meldung ab, statt spaeter kryptisch zu scheitern.
|
||||
missing = []
|
||||
if not cfg.s3_endpoint:
|
||||
missing.append("S3_ENDPOINT (oder s3.endpoint)")
|
||||
if not cfg.s3_access_key:
|
||||
missing.append("S3_ACCESS_KEY")
|
||||
if not cfg.s3_secret_key:
|
||||
missing.append("S3_SECRET_KEY")
|
||||
if not cfg.s3_bucket:
|
||||
missing.append("S3_BUCKET (oder s3.bucket)")
|
||||
if not cfg.pg_dsn:
|
||||
missing.append("PG_DSN")
|
||||
if cfg.enable_deermapper_api and not cfg.deermapper_api_key:
|
||||
missing.append("DEERMAPPER_API_KEY (enable_deermapper_api ist aktiv)")
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
"Fehlende Konfiguration/Secrets: " + ", ".join(missing)
|
||||
+ ". Bitte in der .env / als Umgebungsvariablen setzen."
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Helpers
|
||||
@@ -200,13 +223,18 @@ def ocr_extract_timestamp(jpg_bytes: bytes, crop_w_frac: float, crop_h_frac: flo
|
||||
|
||||
bw = gray.point(lambda p: 255 if p > 180 else 0)
|
||||
|
||||
cfg = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789:- '
|
||||
text = pytesseract.image_to_string(bw, config=cfg).strip()
|
||||
tess_cfg = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789:- '
|
||||
text = pytesseract.image_to_string(bw, config=tess_cfg).strip()
|
||||
|
||||
# retry without thresholding
|
||||
text2 = pytesseract.image_to_string(gray, config=cfg).strip()
|
||||
|
||||
dt_local_naive = _parse_ocr_datetime(text, text2)
|
||||
try:
|
||||
# Schnellpfad: der Schwellwert-Durchlauf reicht meist -> zweiten (teuren)
|
||||
# OCR-Lauf einsparen.
|
||||
dt_local_naive = _parse_ocr_datetime(text)
|
||||
except ValueError:
|
||||
# Fallback: ohne Thresholding erneut lesen und beide Durchlaeufe parsen
|
||||
# (identisches Verhalten wie zuvor).
|
||||
text2 = pytesseract.image_to_string(gray, config=tess_cfg).strip()
|
||||
dt_local_naive = _parse_ocr_datetime(text, text2)
|
||||
|
||||
# Keep camera-local timezone (no UTC conversion requested)
|
||||
return dt_local_naive.replace(tzinfo=customer_tzinfo)
|
||||
|
||||
Reference in New Issue
Block a user