feat: harden production security boundaries

This commit is contained in:
ik
2026-09-07 07:51:25 +07:00
parent 2fa6b68279
commit 687b4c9cb5
13 changed files with 105 additions and 11 deletions
+4 -2
View File
@@ -3,7 +3,9 @@ WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY apps/api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY apps/api .
COPY rf4_research ./rf4_research
RUN useradd --create-home --uid 10001 rf4
COPY --chown=rf4:rf4 apps/api .
COPY --chown=rf4:rf4 rf4_research ./rf4_research
USER rf4
EXPOSE 8000
CMD ["sh", "-c", "alembic upgrade head && python -m app.seed && uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log"]
+6 -1
View File
@@ -54,6 +54,10 @@ async def structured_request_log(request: Request, call_next):
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
if request.url.path.startswith("/api/v1/admin/") or request.url.path == "/api/v1/catch-reports":
response.headers["Cache-Control"] = "no-store"
if settings.deployment_environment == "production":
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
@@ -163,7 +167,8 @@ def records(
def _admin(authorization: Annotated[str | None, Header()] = None) -> str:
if not authorization or authorization != f"Bearer {settings.admin_token}":
expected = f"Bearer {settings.admin_token}"
if not authorization or not hmac.compare_digest(authorization, expected):
raise HTTPException(status_code=401, detail="invalid admin token", headers={"WWW-Authenticate": "Bearer"})
return "admin"
+4
View File
@@ -59,6 +59,8 @@ def test_liveness_does_not_probe_dependencies() -> None:
response = client.get("/health?token=must-not-be-logged")
assert response.json() == {"status": "ok"}
assert len(response.headers["X-Request-ID"]) == 32
assert response.headers["X-Frame-Options"] == "DENY"
assert response.headers["Cross-Origin-Opener-Policy"] == "same-origin"
def test_spot_detail_and_catches() -> None:
@@ -80,11 +82,13 @@ def test_records_list_is_empty_before_import() -> None:
def test_user_report_requires_moderation_before_activity() -> None:
created = client.post("/api/v1/catch-reports", json={"fish_slug": "pike", "waterbody_slug": "test-lake", "x": 77, "y": 88, "weight_g": 5500, "bait_name": "Новая приманка", "player_name": "Reporter"})
assert created.status_code == 201
assert created.headers["Cache-Control"] == "no-store"
assert created.json()["moderation_status"] == "pending"
report_id = created.json()["id"]
headers = {"Authorization": "Bearer change-me-in-production"}
pending = client.get("/api/v1/admin/catch-reports", headers=headers)
assert pending.status_code == 200
assert pending.headers["Cache-Control"] == "no-store"
assert any(item["id"] == report_id for item in pending.json())
approved = client.patch(f"/api/v1/admin/catch-reports/{report_id}", headers=headers, json={"status": "approved", "reason": "fixture verified"})
assert approved.status_code == 200
+2
View File
@@ -13,5 +13,7 @@ ENV HOST=0.0.0.0 PORT=4321 NODE_ENV=production
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
RUN chown -R node:node /app
USER node
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]