Перейти к содержимому

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

PrefixScopeAccessExample
/_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_accountper-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_TOKEN
razmakh://prod/_internal/postgres/PASSWORD
razmakh://prod/_internal/awg/PRIVATE_KEY
# Per-organization (RBAC scoped в Infisical)
razmakh://prod/org-abc123/marketplace-wb-shop-1/wb_token/CONTENT_API
razmakh://prod/org-abc123/marketplace-wb-shop-1/wb_token/ANALYTICS_API
razmakh://prod/org-abc123/integrations/telegram_bot/BOT_TOKEN
razmakh://prod/org-abc123/integrations/1c/PASSWORD
# Shared constants (no PII)
razmakh://prod/shared/wb-api/RATE_LIMITS
razmakh://prod/shared/llm-routing/DEEPSEEK_API_KEY

Python usage

Env-based singleton (production default)

from razmakh_core.secrets import InfisicalClient
# Reads INFISICAL_API_URL / INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_ID
client = 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=True bypass cache
  • client.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), 4xx other

Token lifecycle

  • Universal Auth expiresIn (default 30 дней per Infisical config)
  • Auto-refresh за 60s до expiry
  • On 401 mid-request — token invalidated, refresh on next call

Environment variables (production backend)

Окно терминала
INFISICAL_API_URL=https://secrets.razmakh.ru
INFISICAL_CLIENT_ID=<machine identity client_id>
INFISICAL_CLIENT_SECRET=<machine identity client_secret>
INFISICAL_PROJECT_ID=ecc6e2c6-6b3a-4706-b2b6-fa0978e423bf

P17: Эти 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.md F-03
  • Tests: packages/core/tests/test_infisical_client.py + test_secret_ref.py (26 tests)