Security Model
SeptemCore is designed on the principle that everything is encrypted, always, everywhere — without exceptions. Security is not a feature; it is a structural property of the platform enforced at every layer.
1. Seven-Layer Encryption
Every data pathway has its own encryption guarantee. The layers are independent — compromising one does not compromise others.
| Layer | What is encrypted | Standard | Implementation |
|---|---|---|---|
| At rest | Databases, files, backups, logs | AES-256 | PostgreSQL TDE, S3 server-side encryption, ClickHouse encrypted volumes |
| In transit (external) | All HTTP, WebSocket, gRPC (client-facing) | TLS 1.3 | HTTPS-only, HSTS, certificate pinning |
| In transit (internal) | Service-to-service communication | mTLS | Mutual TLS via Istio service mesh |
| Field-level (PII) | Email, phone, IP address columns | AES-256-GCM | Column-level encryption in PostgreSQL (shared/crypto) |
| Envelope | Encryption keys (DEK, KEK) | RSA-2048+ | DEK → KEK → Master Key (HSM) |
| Secrets | API keys, passwords, tokens | — | HashiCorp Vault only — never in code, ENV, or git |
| Backups | All backup archives | AES-256 | Encrypted before upload, independent key rotation |
2. Key Hierarchy
All cryptographic material flows through a three-tier envelope encryption scheme:
Master Key (HSM — FIPS 140-3 Level 3)
└── KEK — Key Encryption Key (HashiCorp Vault)
└── DEK — Data Encryption Key (application layer)
- Master Key lives in a Hardware Security Module certified to FIPS 140-3 Level 3. It never leaves the HSM in plaintext.
- KEK is managed by HashiCorp Vault. Vault uses the HSM to unwrap it on startup and serve it to authorised services.
- DEK is generated per data classification (e.g. one DEK per tenant PII store). The application holds the DEK in memory only — never on disk or in environment variables.
Rotation policy: Automatic 90-day rotation for all keys. Dual-key strategy: During rotation the old key remains valid for decryption while the new key signs all new writes. Zero downtime, zero manual steps.
3. JWT Signing Key Lifecycle
JWT access tokens are signed with ES256 (ECDSA P-256). The signing key is managed through Vault with a strict in-memory caching strategy:
| Phase | Behaviour |
|---|---|
| INIT | IAM service fetches the JWT signing key from Vault and loads it into process memory. Startup fails if Vault is unreachable. |
| Runtime | Every token signature uses the in-memory key — no per-request Vault round-trip. |
| Rotation | Vault pushes a watch notification → IAM loads the new key into memory. Dual-key window: old key continues to validate existing tokens; new key signs all new tokens. |
| Vault runtime outage | IAM continues with the current in-memory key. Rotation is blocked. An alert fires via system.health Notify channel. |
| Vault prolonged outage | After KEY_MAX_AGE (default: 7 days) without successful rotation, IAM enters DEGRADED mode and fires a critical level alert. |
Token characteristics:
| Property | Value |
|---|---|
| Algorithm | ES256 (ECDSA P-256) |
| Access token TTL | 15 minutes |
| Refresh token TTL | 7 days |
| Max JWT size | 8 KB (HTTP header budget) |
| PII in claims | Prohibited — JWT is base64-readable without the key |
4. Password Security — Argon2id
Passwords are hashed with Argon2id (OWASP 2024 recommendation).
bcrypt is prohibited for new implementations.
// services/iam/internal/service/auth_service.go
import "golang.org/x/crypto/argon2"
const (
ArgonTime = 1 // iterations
ArgonMemory = 64 * 1024 // 64 MB
ArgonThreads = 4
ArgonKeyLen = 32
)
func hashPassword(password string, salt []byte) []byte {
return argon2.IDKey(
[]byte(password), salt,
ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen,
)
}
- Salt: 16 bytes, cryptographically random (
crypto/rand). - Timing-safe comparison (
subtle.ConstantTimeCompare) — prevents timing side-channel attacks. - Anti-enumeration: login, register, and password-reset responses use identical timing and identical error messages for valid and invalid accounts.
5. mTLS — Service-to-Service Security
All internal gRPC traffic between services uses mutual TLS. Each service presents a client certificate; the server validates it before processing any RPC.
Client service ──── TLS handshake (both sides present cert) ──── Server service
↑ certificates managed by Istio / Vault SDS
| Property | Value |
|---|---|
| Protocol | mTLS (both client and server authenticate) |
| Certificate authority | Internal CA managed by HashiCorp Vault PKI |
| Certificate rotation | Automatic (Istio Citadel / Vault agent sidecar) |
| Implementation | Istio service mesh sidecar injection; shared/mtls helpers for non-sidecar scenarios |
No service can call another without a valid certificate issued by the platform CA. An attacker who compromises the network cannot impersonate a service without the corresponding private key.
6. Module Sandbox — CSP and Isolation
Third-party module code runs inside the UI Shell under a strict Content Security Policy. The Shell enforces the following rules:
| Threat | Defence |
|---|---|
| Malicious module injecting scripts | Strict CSP — script-src 'self' cdn.septemcore.com — external script sources are blocked |
| XSS between modules (cross-MFE) | Sandbox isolation: each MFE runs in its own React tree; no direct DOM access across module boundaries |
| Supply chain attack via npm | SCA scanning (Snyk) on every module bundle before it is accepted by the Module Registry |
| Unauthorised module loading | Module Registry validates a digital signature on every bundle before it is served to the Shell |
| Data exfiltration via fetch | CSP connect-src restricts outgoing XHR/fetch to api.septemcore.com and the tenant's own domain |
Module Signing Flow
1. Developer builds module (pnpm build)
2. CLI signs bundle: kernel-cli sign --key developer.pem remoteEntry.js
3. CLI uploads to Module Registry: POST /api/v1/modules/install
4. Module Registry verifies signature against developer's registered public key
5. Signature valid → bundle stored in S3 and CDN URL written to manifest
6. Signature invalid → 422 Unprocessable Entity — bundle rejected
Modules signed by verified publishers receive a verified badge in
the Admin module catalogue. Unsigned modules can only be installed
by Platform Owners (development workflow).
7. Row-Level Security — Tenant Isolation
Every table that stores tenant data has a PostgreSQL Row-Level Security policy enforced at the database engine level. No application code change can bypass it:
-- Applied to every module data table during migration
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON contacts
USING (tenant_id = current_setting('app.tenant_id')::uuid);
The application sets app.tenant_id from the JWT tenantId claim at
the beginning of every database session. Even if a bug in application code
constructs a malformed query, PostgreSQL silently filters out rows that
do not belong to the current tenant.
ClickHouse double isolation: Because ClickHouse does not support RLS natively, the platform enforces isolation at two levels:
- ClickHouse Row Policy —
CREATE ROW POLICYon each table. - Application-layer WHERE clause — every ClickHouse query generated
by the Data Layer service unconditionally appends
WHERE tenant_id = ?using the tenant ID from the JWT context.
8. Secret Management — HashiCorp Vault
No secret ever appears in:
- Source code
- Environment variables in any deployment manifest
- Docker images
- Git history
All secrets are fetched at service startup via the Vault agent sidecar or
the vault shared library (services/vault/):
// services/vault/vault.go (simplified)
type Client struct {
client *vault.Client
}
func (c *Client) GetSecret(ctx context.Context, path string) (string, error) {
secret, err := c.client.KVv2("secret").Get(ctx, path)
if err != nil {
return "", fmt.Errorf("vault get secret %s: %w", path, err)
}
return secret.Data["value"].(string), nil
}
Secrets used by the platform:
| Secret | Vault path | Consumer |
|---|---|---|
| JWT signing key (ES256 private key) | secret/iam/jwt-signing-key | IAM service |
| PostgreSQL credentials | secret/{service}/postgres | Each Go service |
| Kafka credentials | secret/kafka | Event Bus, Audit |
| AES-256-GCM DEK | secret/crypto/field-dek | shared/crypto |
| TLS certificates | pki/issue/{service} | All services (mTLS) |
| Integration provider credentials | secret/integrations/{tenantId}/{providerId} | Integration Hub |
9. Transport Security
| Property | Requirement |
|---|---|
| External TLS version | TLS 1.3 (TLS 1.2 allowed for legacy clients; 1.0 and 1.1 prohibited) |
| HSTS | max-age=31536000; includeSubDomains; preload |
| Certificate authority | Let's Encrypt (ACME) for public endpoints; internal Vault PKI for services |
| SSL renewal | 30 days before expiry — automatic via ACME or Vault PKI |
| Certificate storage | Private keys in HashiCorp Vault — never on disk or in K8s Secrets |
Custom domains provisioned via the Domain Resolver service use the same Let's Encrypt ACME pipeline. Envoy Secret Discovery Service (SDS) fetches certificates on demand from Vault — Envoy never loads all certificates at startup (lazy fetch prevents memory bloat at scale).
10. Threat Model Summary
| Threat | Primary defence | Secondary defence |
|---|---|---|
| Malicious module code | CSP, module signing | SCA scan (Snyk) |
| XSS between MFEs | Sandbox isolation, CORS | React error boundaries |
| Supply-chain attack | SCA on every bundle | Pinned exact versions (no ^) |
| Cross-tenant data leak | PostgreSQL RLS | ClickHouse Row Policy + app WHERE |
| JWT forgery | ES256 signature (HSM-backed key) | Vault key rotation every 90 days |
| Password brute force | Argon2id (64 MB / 1 iteration) | Rate limit 5/min per IP+email |
| Secret exposure | Vault agent — never in ENV | Sealed Secrets in git (Kubeseal) |
| Service impersonation | mTLS (both sides authenticated) | Istio SPIFFE/SVID identity |
| DDoS | Cloudflare/Akamai edge + rate limiting | Token-bucket per-tenant 1 000 RPS |
| Insider threat / API abuse | Full audit trail, immutable ClickHouse | GDPR anonymization, 7-year retention |