[Security]

API keys, CORS, body limits, secrets hygiene, Fernet metadata keys, and threat-aware defaults for PYNE ops.

Security

Abstract

PYNE security is layered: transport/hosting (Cloud Run, containers), application (API keys, admin tokens, CORS, body size caps), build secrets (Fernet metadata keys, VSCE PAT), and supply chain (CI dependency installs, extension packaging). This page documents controls that exist in code and workflows — not a full threat model certification.

Conceptual model

Interface surface

HTTP application controls (backend/app.py)

ControlDefault / mechanism
Body sizeMAX_CONTENT_LENGTH = 5 * 1024 * 1024 (5 MiB) — DoS/OOM mitigation
CORSALLOWED_ORIGINS env (comma-separated); default includes product hosts + localhost regex
MethodsGET, POST only via flask-cors config
HeadersContent-Type, Authorization, X-Admin-Token
Credentialssupports_credentials=False

API keys (backend/middleware/auth.py)

  • Keys stored as hashes, not raw secrets (store path via API_KEY_STORE)
  • Tiers: free, hobby (5k), pro (50k), team (200k), enterprise (∞)
  • Rate limit by monthly call counters (calls_used / calls_limit)
  • Decorators: require_api_key, require_admin_token

Build / release secrets

SecretPurpose
METADATA_KEY / _METADATA_KEYStable Fernet key for LSP metadata encrypt
VSCE_PATVS Code Marketplace / vsce package
GITHUB_TOKENRelease asset upload
PYNESCRIPT_METADATA_KEYRuntime decrypt env fallback

Container hardening (Dockerfile target api)

  • Multi-stage build (no compiler toolchain in final image)
  • Non-root appuser (uid 1000)
  • Production env flags + gunicorn entrypoint
  • Healthcheck without privileged ops
  • Intended mutable path /data (key store); prod compose uses SQLite there for multi-worker

Admin minting

  • POST /auth/create_key requires ADMIN_TOKEN env and matching X-Admin-Token header (constant-time compare)
  • Unset ADMIN_TOKEN → 403 (fail closed, not open access)

Key store backends (STORE_BACKEND)

ValueUse
json (default)Single-process / tests; file may hold raw keys
sqliteMulti-worker single host (shared volume); hash-only
redisMulti-replica; needs REDIS_URL; hash-only

Internals

PathRole
backend/app.pyCORS, body limit, blueprint registration
backend/middleware/auth.pyKey store selection, tiers, decorators, admin gate
backend/middleware/key_store_redis.py / key_store_sqlite.pyHash-only shared stores
backend/middleware/schemas.pyRequest validation schemas
src/pynescript/langserver/providers/metadata_decrypt.pyEncrypted metadata load
.github/workflows/*.ymlSecret consumption
frontend security testsbun run test:security in CI

Audit notes encoded in source

Comments in app.py reference a 2026-07-05 audit (findings S1 body size, S7/S8 CORS). Prefer reading the code over assuming defaults never change.

Invariants & edge cases

  1. Cloud Run --allow-unauthenticated does not mean open data. It means IAM does not gate invoke; the Flask app must.
  2. Free tier with calls_limit=0 is special-cased as unlimited remaining in some helpers — verify tier semantics before production billing.
  3. Localhost CORS regex permits any port on localhost/127.0.0.1 — fine for desk, wrong for prod ALLOWED_ORIGINS.
  4. Fernet protects binary metadata packaging, not user script confidentiality.
  5. Admin token routes are high privilege — rotate and store separately from user API keys.
  6. Never log raw API keys. Hash lookups only.

Worked examples

Harden CORS for a single origin

export ALLOWED_ORIGINS=https://app.example.com
python -m backend.app

Point key store at a volume

export API_KEY_STORE=/data/api_keys.json
# ensure process user can write /data

Rotate metadata key (ops procedure sketch)

  1. Generate new Fernet key; store as secret METADATA_KEY
  2. Rebuild LSP with CRYPTO_KEY set
  3. Ship new binary; retire old key after client upgrade window
  4. Do not leave dual-key decrypt unless you implement it

Failure modes

SymptomCauseFix
Browser CORS errorOrigin not listedSet ALLOWED_ORIGINS
413 Payload Too LargeBody > 5MBReduce OHLCV batch; raise limit knowingly
401/403 on /runMissing/invalid keyCheck Authorization header scheme
Rate limitedTier exhaustedUpgrade tier / wait reset
Secret in fork PR logsMisconfigured workflow echoNever echo secrets; use masked env
Root-owned files in volumeOld image ran as rootAlign UID with appuser

See also