Secrets architecture
P17: Automated secrets protection. Никогда вручную с secrets. P1+P2+P11: razmakh Infisical standalone, не Hub vault Netnik.
Stack
- Storage: Infisical self-hosted на razmakh-vps (https://secrets.razmakh.ru)
- Backend DB: PostgreSQL 16 (separate от main razmakh PG 18), Redis cache
- Auth: Universal Auth (M2M) — Machine Identity Client ID + Client Secret
- Encryption at rest: aes-256 (Infisical native)
- Encryption in transit: TLS 1.3 (Caddy via Let’s Encrypt DNS-01)
- Client lib:
razmakh_core.secrets.InfisicalClient(custom httpx-based, не Infisical SDK) - Cache: TTL 120s in-process + stale-while-error 5 min grace
- Retry: exponential backoff 3 attempts (1s → 2s → 4s)
secret_ref URI format
razmakh://<environment>/<path>/<KEY>Components:
<environment>— Infisical env slug (prod,staging,dev)<path>— folder path в Infisical project (например/_internal/cloudflare)<KEY>— secret name (uppercase by convention)
Path naming convention
| Prefix | Scope | Access | Example |
|---|---|---|---|
/_internal/ | Operational creds (admin only) | razmakh-backend identity | /_internal/cloudflare/API_TOKEN |
/org-<uuid>/ | Per-organization (P15 scope) | per-org identity (Phase 1.5+) | /org-abc/auth/SESSION_KEY |
/org-<uuid>/marketplace-<kind>-<slug>/ | Per marketplace_account | per-org identity | /org-abc/marketplace-wb-shop-1/wb_token/CONTENT_API |
/shared/ | Cross-org constants (никакого PII) | all backend identities | /shared/wb-api/RATE_LIMITS |
P15: org-<uuid> scope = organization_id (RLS context var). НЕ seller_uuid (legacy WBPulse). См. netnik-state/decisions/razmakh-rls-scope-organization-2026-05-17.md.
Example secret refs
# Operational (admin / ops agents only)razmakh://prod/_internal/cloudflare/API_TOKENrazmakh://prod/_internal/postgres/PASSWORDrazmakh://prod/_internal/awg/PRIVATE_KEY
# Per-organization (RBAC scoped в Infisical)razmakh://prod/org-abc123/marketplace-wb-shop-1/wb_token/CONTENT_APIrazmakh://prod/org-abc123/marketplace-wb-shop-1/wb_token/ANALYTICS_APIrazmakh://prod/org-abc123/integrations/telegram_bot/BOT_TOKENrazmakh://prod/org-abc123/integrations/1c/PASSWORD
# Shared constants (no PII)razmakh://prod/shared/wb-api/RATE_LIMITSrazmakh://prod/shared/llm-routing/DEEPSEEK_API_KEYPython usage
Env-based singleton (production default)
from razmakh_core.secrets import InfisicalClient
# Reads INFISICAL_API_URL / INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_IDclient = InfisicalClient.from_env()
cf_token = client.get_secret( project_id=client.default_project_id, environment="prod", path="/_internal/cloudflare", secret_key="API_TOKEN",)secret_ref helper (для config-driven секретов)
from razmakh_core.secrets import resolve_secret_ref
cf_token = resolve_secret_ref("razmakh://prod/_internal/cloudflare/API_TOKEN")Manual instantiation (для tests или multi-project clients)
client = InfisicalClient( api_url="https://secrets.razmakh.ru", client_id="...", client_secret="...", default_project_id="ecc6e2c6-...",)Behavior guarantees
Cache (P22 runtime configurability)
- TTL: 120 секунд (constant
_SECRET_TTL_SECONDS) - Per-process LRU 512 entries
- Cache key:
{project_id}|{environment}|{path}|{secret_key} force_refresh=Truebypass cacheclient.invalidate_cache()— manual clear (e.g. при rotation event)
Stale-while-error (P19 chaos resilience)
При Infisical down (network error / 5xx) и cache entry в stale window (120s < age < 720s):
- Возвращается stale value (WARN log)
- Application продолжает работать
- При первом успешном fetch — cache refreshes
При age > 720s — exception propagates (no fallback, fail-fast).
Retry (transient network resilience)
- 3 попытки (1s → 2s → 4s exponential)
- Retry на:
ConnectError,ReadError,RemoteProtocolError,HTTPStatusError(5xx) - NO retry на:
401(auth fail),404(not found),4xxother
Token lifecycle
- Universal Auth
expiresIn(default 30 дней per Infisical config) - Auto-refresh за 60s до expiry
- On
401mid-request — token invalidated, refresh on next call
Environment variables (production backend)
INFISICAL_API_URL=https://secrets.razmakh.ruINFISICAL_CLIENT_ID=<machine identity client_id>INFISICAL_CLIENT_SECRET=<machine identity client_secret>INFISICAL_PROJECT_ID=ecc6e2c6-6b3a-4706-b2b6-fa0978e423bfP17: Эти 4 переменные единственные secrets в production env. Остальные — через secret_ref:// resolution at runtime.
INFISICAL_CLIENT_ID + INFISICAL_CLIENT_SECRET хранятся:
- Backend: GitHub Actions secrets → systemd EnvironmentFile через CI/CD deploy (Phase 1 F-19’)
- Dev: Direnv
.envrcлокально (gitignored)
Per-organization secret scoping (Phase 1.5+)
В Phase 1 — один backend identity (razmakh-backend) с Admin role. Reads все secrets.
В Phase 1.5 при multi-org (Block 1.5.1):
- Per-org backend identity создаётся при organization onboarding
- RBAC policy: identity X может read только
/org-<uuid_X>/** - Cross-tenant probe в CI (Block 1.5.5) — synthetic identity X пытается read
/org-Y/...→ 403 expected
См. plan 1.5-multi-org-partners.md Block 1.5.5.
Rotation strategy
См. netnik-state/decisions/razmakh-pre-launch-credentials-rotation-2026-05-17.md (Round 1 в Block 1.5.0 перед first partner invite).
Deferred (Phase 1.5+)
- Per-organization RBAC enforcement test (Block 1.5.5 — cross-tenant probe)
- Chaos test: Infisical container down 5 min (Block 1.5.5 — synthetic infra failure)
- Encryption key rotation runbook (Round 2 = Phase 3)
- Webhook на rotation events →
client.invalidate_cache()automatic - Audit log integration (Phase 1 F-10 observability)
Sources
- Decision:
razmakh-secret-backend-infisical-2026-05-17.md(Hub vault → Infisical correction) - Decision:
razmakh-rls-scope-organization-2026-05-17.md(organization_id, не seller_uuid) - Plan:
plan/active/00-foundation.mdF-03 - Tests:
packages/core/tests/test_infisical_client.py+test_secret_ref.py(26 tests)