Security checklist
A per-module audit checklist focused on what an attacker or careless operator could exploit. Use it before exposing a Titan backend publicly, during a pen-test prep, or as compliance evidence.
The checklist below treats each module as independently hostile — the box ticks the same whether you're securing a single API pod or a 50-pod fleet.
Surface-area overview
| Module | External attack surface | Primary risk |
titan-auth | JWT verification endpoint, JWKS URL | Algorithm confusion, key rotation |
titan-cache | None directly (L1/L2 reads) | Sensitive data persistence in L2 (Redis) |
titan-database | SQL execution path | Injection, missing RLS, leaked DSN |
titan-discovery | Registry writes via Redis | Spoofed registration, registry pollution |
titan-events | Event payload deserialization | PII leakage, untrusted handlers |
titan-health | /healthz/full JSON (stack info) | Information disclosure |
titan-lock | Lock keys in Redis | Lock pinning, ownership spoofing |
titan-metrics | /metrics exposition + storage | Cardinality abuse, label PII |
titan-notifications | Outbound webhooks, channel queue | Webhook forgery, template injection |
titan-pm | Child process spawn parameters | Argv-leaked secrets, file-descriptor inherit |
titan-ratelimit | Per-key state | IP spoofing without trusted proxy |
titan-redis | Direct Redis client | Plaintext AUTH, shared keyspaces |
titan-scheduler | Stored cron expressions | Untrusted cron → forced load |
titan-telemetry-relay | WAL file, transport messages | File permissions, unsigned transport |
config (built-in) | Config sources | Secrets in committed defaults |
logger (built-in) | Log sinks | PII in structured logs |
Per-module hardening
titan-auth
official · @omnitron-dev/titan-auth
Algorithm pinned explicitly. Never accept whatever the incoming JWT declares; configure
algorithm: 'RS256'(or'ES256'/'HS256') and enforce it. JWT spec history is littered with algorithm-confusion CVEs.JWT secret length & source. HS256 secrets ≥ 32 bytes, loaded from secret manager — never from a committed file or default value.
JWKS rotation supported. For RS256/ES256, configure
jwksUrlwith a short cache TTL (≤ 1 h) so revoked keys propagate.verificationKeysregistry for HS256 rotation. Add new keys before issuing tokens with them; remove old keys only after all in-flight tokens expire.Token cache TTL ≤ token lifetime. Cached verifications bypass JWKS fetch; never cache longer than the token's
exp.Clock skew bounded.
clockTolerance(seconds, or a jose string like'5s') defaults to 0 — no skew is allowed at all, so a token from a server whose clock runs slightly ahead fails itsnbf/iatcheck and one expiring this instant failsexp. Set it only as wide as the fleet's clock discipline requires: the window is also how long an expired token stays usable.audienceandissuerenforced. Both options must be populated; otherwise any well-signed token from any service authenticates.InvalidTokenError/TokenExpiredErrornot logged with the token body. Logcodeandkidonly.No JWT in URLs. Use the
Authorization: Bearerheader. URLs end up in proxy logs, browser history, referer headers.
titan-cache
official · @omnitron-dev/titan-cache
Don't cache sensitive payloads in L2. L2 is Redis; Redis persistence means caches survive process restart and may be backed up. Use L1-only for PII / auth state.
Key namespacing per tenant.
users:{tenantId}:{userId}— never a bareusers:{userId}if you have multi-tenant data.TTL always set. Default to
defaultTTL; unbounded keys drift forever.Cache stampede protection. Use
getOrSet(singleflight) notget+ manualseton miss.No cache-key from user input without hashing/length cap. Multi-MB keys are a DOS vector against the cache itself.
titan-database
official · @omnitron-dev/titan-database
Parameterised queries only. Kysely query builder is parameterised by default. If you reach for
sql.raw(), whitelist inputs.RLS enabled on multi-tenant tables. Use
defineRLSSchema+rlsPlugin— don't rely on application-layer WHERE clauses to enforce isolation.validateColumnNameson dynamic queries. When column names come from a request, usevalidateColumnNamesto block injection.DSN from secret manager.
connectionStringshould resolve viaConfigServicefrom a secrets source, not a literal in code.Least-privileged DB user. App connects as a role with only the privileges it needs; migrations run as a separate role.
enableDatabaseIndicator: trueontitan-healthso the probe surfaces a dead DB before the load balancer keeps routing.Backup encryption. Out of scope here but: rotated encryption keys, off-site copies, restore drills.
titan-discovery
official · @omnitron-dev/titan-discovery
Redis ACL on the discovery prefix. The pod's Redis user has
HSET/EXPIRE/PUBLISHontitan:discovery:*only — no full-keyspace access.Network segmentation. Discovery's Redis is cluster-internal; never reachable from the public network.
Heartbeat TTL ≥ 3 × interval. Premature ejection from the registry causes thundering reconnects when a pod is slow, not dead.
clientMode: truefor read-only consumers. Any pod that doesn't host services shouldn't be able to register fake nodes.Don't expose
discovery@…over public RPC. Internal topology is sensitive information.
titan-events
official · @omnitron-dev/titan-events
Schema validation on every event channel. Untyped payloads from one service to another are a wide attack surface.
No PII in events. Events are often logged or persisted in history — assume any payload may end up in a SIEM.
Bounded handler concurrency. A slow handler shouldn't back-pressure the entire bus — use the per-handler concurrency option.
History TTL bounded.
enableHistory: truewith no TTL is an unbounded memory growth path.
titan-health
official · @omnitron-dev/titan-health
/healthzand/readyzexposed; full JSON gated. The detailedcheck()output reveals component names, error messages, and the topology — operator-only.enableRpcService: falseif you serve probes via HTTP and don't want RPC inspection.Indicator timeout configured. A hung indicator blocks the whole probe — set
timeout≤ probe period (default 5 s).No secrets in indicator messages. Don't return the DSN in a "database unhealthy" message.
titan-lock
official · @omnitron-dev/titan-lock
TTL always set. A crashed holder shouldn't pin the lock forever. Set TTL to the longest credible work time + margin.
Ownership UUID enforced.
acquireLock(key, ttlMs)returns a generatedlockId(UUID); pass it back toreleaseLock(key, lockId)/extendLock(key, lockId, ttlMs). The included Lua scripts verify thelockIdon release. Never release withDEL keydirectly. PreferwithLock(key, fn)so release is automatic.Extend, don't reset. Use
extendLock(key, lockId, ttlMs)to push the TTL out periodically while still holding; don't release-and-reacquire, which is racy.Lock granularity. A single global lock per resource is cleaner than a hierarchy; deep lock chains invite deadlocks.
titan-metrics
official · @omnitron-dev/titan-metrics
/metricsendpoint authenticated. Prometheus exposition reveals throughput, error rates, internal endpoints — gate behind an operator token.No PII in label values.
{ userId }creates a fresh series per user — both a cardinality explosion and a PII leak. Keep labels totier,region,status.appNameset. Untagged metrics from multiple services collide.Storage backend access controlled. Postgres-backed metrics give whoever has DB access full historical visibility.
titan-notifications
official · @omnitron-dev/titan-notifications
Webhook signature secret rotated. Configure
webhookConfig.signatureSecretand verify on the receiver.Webhook timeout bounded. Default 5 s; never wait indefinitely on a third-party endpoint.
Template engine input escaped. Templates receive attacker-controlled data via
payload.data; ensure the template engine escapes context-appropriately (HTML for email, plain for SMS).Per-recipient rate limit. Set
rateLimiterConfig.defaultLimitsso a compromised account can't spam the world via your service.Preferences honoured before send.
IPreferenceStoremust check unsubscribes / DND windows before delivery, not after.DLQ access controlled.
subscribeToDLQ()/requeueFromDLQ()expose undelivered payloads — operator-only.
titan-pm
official · @omnitron-dev/titan-pm
Secrets via env, not argv. Process arguments appear in
ps,/proc/<pid>/cmdline, and crash dumps. Pass secrets viaenv: { SECRET: '...' }.stdioignored or piped, never'inherit'for children handling secrets. Inheritance leaks the child's output into the parent's stdout.Restart limits set. Without
maxRestarts, a crash loop is a CPU DoS vector against the supervisor.Health-check command bounded. There is no
healthCheckTimeout. Per-process monitoring takeshealth: { interval, timeout, retries }(timeout default 5000 ms); the pool's own check has no timeout field of its own and is bounded instead by the service proxy'srequestTimeout(default 30000 ms), which caps every RPC. Set both deliberately — the pool serialises its checks, so one slow worker delays the whole sweep by up torequestTimeout.No untrusted command paths.
commandmust resolve to a known binary — never to a user-supplied path.Audit
pm@1.0.0if exposed. PM control = code execution. Admin role minimum, mutual TLS preferred.
titan-ratelimit
official · @omnitron-dev/titan-ratelimit
Per-IP keys behind trusted proxy. Use the proxy-supplied client IP (e.g.,
X-Forwarded-Forparsed by a trusted-proxies middleware), not the socket peer.Key includes user / API key when authenticated. A bare IP key punishes shared NATs.
queueEnabled: falsefor unauthenticated endpoints. Queueing turns rejections into work — a DoS amplifier.Redis-backed for multi-pod. In-memory limits are per-pod; rotating pods lets the attacker reset.
Distinct keyspaces per app. Configure
keyPrefixso different services don't share counters.
titan-redis
official · @omnitron-dev/titan-redis
TLS to managed Redis. Configure
tls: {}in the client options; verify the certificate.AUTHpassword set, rotated. From secret manager, not hardcoded.DB-per-concern split. Cache, queues, rate limits, locks, discovery — separate
db:indexes (see the module map table).enableReadyCheckandlazyConnectaligned with reconnect policy.Offline queue bounded. There is no
commandQueueMaxLength— not in this module and not in ioredis. What prevents the memory blow-up while Redis is unreachable isenableOfflineQueue: false(fail commands immediately instead of buffering them) andmaxRetriesPerRequest, both passed through to ioredis.Disable dangerous commands. Server-side
rename-commandforFLUSHALL,FLUSHDB,KEYS,CONFIGin shared environments.
titan-scheduler
official · @omnitron-dev/titan-scheduler
No user-supplied cron expressions.
@Cronstrings are developer-authored. If you accept schedules from users (multi- tenant SaaS), validate against an allow-list of patterns.Job runtime bounded. Long-running jobs without a timeout pile up missed-fires; configure
timeoutper job.Persistence backend access controlled. Same access story as the database: persisted job rows reveal what runs and when.
@Cronhandlers idempotent. Missed-fire replay semantics make some jobs run twice; design for it.
titan-telemetry-relay
official · @omnitron-dev/titan-telemetry-relay
WAL file permissions
0600. WAL contains buffered metric / log payloads — readable WAL = readable observability.Signed transport between producer and aggregator. Run over a TLS Netron transport or a mutually authenticated channel.
Aggregator access controlled. Whoever can read the aggregator's storage can read everyone's telemetry.
config (built-in)
built-in · @omnitron-dev/titan/module/config
No secrets in committed files.
config/default.yamlis defaults;config/local.yamlis git-ignored; secrets come from env or a secret-manager source.Schema covers every required key. Misconfiguration must fail at boot, not at the first dependent call.
watchForChanges: falsein production for connection strings. Hot-reloading a DB URL won't rebind an open pool — reserve hot-reload for safe-to-swap values (log level, flags).Read through
ConfigService, neverprocess.envdirectly. Bypasses layered sources and breaks tests.
logger (built-in)
built-in · @omnitron-dev/titan/module/logger
PII redaction processor enabled. Configure the redaction processor for fields that may carry email / phone / card data.
Log level production-appropriate.
infoorwarnin production;debugonly for time-bounded investigations.No raw secrets in
error.message. Errors are caught and logged; ensure secret-laden messages are sanitised before throwing.Sink configured for tamper-evident transport. Append-only, signed transports for compliance scenarios.
Cross-cutting
Transport / network
Netron port not publicly exposed without auth + TLS.
Cluster-internal services pinned to private VPC.
Egress controlled. Outbound webhooks / JWKS fetches whitelisted.
Secrets management
All secrets via secret manager (AWS Secrets Manager, Vault, etc.). Not in env vars at rest in container images.
Secret rotation runbook tested. Rotating without downtime requires the multi-key support of
titan-auth,titan-redisACL renaming, andConfigServicereload.
Audit logging
Admin actions logged with actor + outcome. Lock force- release, PM admin RPC, manual notification sends, scheduler trigger.
Logs ship to an append-only sink. Local files alone are insufficient for compliance.
Dependency hygiene
pnpm auditclean. Known CVEs in transitive deps patched within SLA.Modules pinned to known versions.
^ranges are fine for dev; freeze on lockfile in CI.
See also
Best Practices / Observability — pairs with this checklist
Tokens & RPC reference — RPC surface specifics
Lifecycle reference — knowing what runs when helps audit