Skip to content

Production Checklist

IdentityScribe ships with safe defaults for development — open ports, no TLS, no authentication. Walk through each section below and change what applies to your deployment.

Security layers

Defense in depth — each layer a request must pass before reaching the application

Network IsolationActive by default

Separates public APIs (port 8080) from internal monitoring (port 9001). Your firewall controls which ports are reachable from the internet.

Connection refused
TLSOFF by default

Encrypts all traffic with TLS 1.2/1.3. Supports server certificates, mutual TLS with client certs, and custom cipher suites.

TLS handshake failure
AuthenticationOFF by default

Validates credentials on every request. Supports JWT bearer tokens, Resource Owner Password (ROPC), LDAP bind, and configured local accounts.

401 Unauthorized
Access RulesON when auth enabled

Evaluates allow/deny rules against the authenticated identity. Deny-by-default when authentication is enabled.

403 Forbidden

Separate internal endpoints from public APIs. Monitoring and health endpoints should not be accessible from the internet.

http {
port = 8080
host = "0.0.0.0"
sockets.internal {
port = 9001
host = "127.0.0.1"
}
}
monitoring.socket = "internal"

With this setup:

  • REST, GraphQL, and MCP channels listen on port 8080 (all interfaces)
  • Prometheus metrics, health probes, and observability endpoints only respond on localhost:9001

Your firewall should block external access to port 9001.

Run HTTPS in production. Never expose plaintext HTTP to untrusted networks.

http.ssl {
enabled = true
cert = "/etc/scribe/tls/server.pem"
key = "/etc/scribe/tls/server.key"
}

Or via environment variables:

Terminal window
SCRIBE_HTTP_SSL_ENABLED=true
SCRIBE_HTTP_SSL_CERT=/etc/scribe/tls/server.pem
SCRIBE_HTTP_SSL_KEY=/etc/scribe/tls/server.key

Require client certificates when you control all API consumers. mTLS (mutual TLS) adds an extra layer of security:

http.ssl {
enabled = true
cert = "/etc/scribe/tls/server.pem"
key = "/etc/scribe/tls/server.key"
ca = "/etc/scribe/tls/client-ca.pem"
client-auth = "REQUIRED"
}

The client-auth options:

  • NONE — No client certificate required (default)
  • OPTIONAL — Request certificate but allow connections without one
  • REQUIRED — Reject connections without a valid client certificate

IdentityScribe defaults to TLS 1.2 and 1.3 with cipher suites from SSL Labs recommendations. These defaults follow current best practices. Only override if you have specific compliance requirements.

ssl {
protocols = [TLSv1.3, TLSv1.2]
cipher-suites = [
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
]
}

By default, authentication is disabled. Enable it for protected deployments. The default access rules deny anonymous access and allow authenticated requests.

Terminal window
SCRIBE_AUTH_ENABLED=true
SCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompany

This configuration:

  • Extracts and validates credentials from every request
  • Returns 401 for requests without valid credentials
  • Validates JWTs against your identity provider’s JWKS
SituationMethodWhy
Modern apps with OAuth/OIDCbearerBearer tokens validated offline, no per-request IdP calls
Legacy apps that send username/passwordropcExchanges credentials for tokens at runtime
Internal tools with LDAP usersldapDirect LDAP bind, no IdP needed
Repeated technical-account accesslocalValidates configured credentials without a backend authentication request

Most deployments should use bearer. Add ropc or ldap when clients cannot obtain tokens themselves, or local for a fixed technical account.

auth {
enabled = true
methods = [bearer]
providers.default {
issuer = "https://auth.example.com/realms/mycompany"
audiences = ["scribe"]
}
}

The defaults work for most setups. Tighten them if your security policy requires it.

Only allow algorithms your IdP actually uses:

auth.jwt.allowed-algorithms = [RS256, ES256]

Never include none in this list. It disables signature verification entirely.

Reject tokens that claim to be valid for more than 8 hours:

auth.jwt.max-lifetime = 480

The value is in minutes. Default is 1440 (24 hours).

This is enabled by default. Keep it that way:

auth.jwt.require-expiration = true

Tokens without exp claims never expire, which is a security risk.

ROPC, opaque bearer-token introspection, and LDAP cache successful authentications. This speeds up repeated requests but delays password changes and account lockouts. Local credentials are checked directly and are not cached.

Caching means:

  • Password changes take effect after cache TTL expires
  • Revoked or locked accounts remain valid until cache expires
  • Reduced load on your IdP or LDAP server
  • LDAP expiry is measured from the successful backend check; a cache hit does not extend it

For tighter security, reduce cache TTL:

auth.cache.ttl = 1m

Or disable completely:

auth.ropc.cache.ttl = 0
auth.ldap.cache.ttl = 0

Disabling the cache means every request hits your IdP or LDAP server. Size your infrastructure accordingly.

When using LDAP for sync or authentication, encrypt the connection.

ldap {
url = "ldaps://directory.example.com:636"
}

Or with StartTLS on the standard port:

ldap {
url = "ldap://directory.example.com:389"
start-tls = true
}

By default, IdentityScribe verifies the LDAP server’s certificate against the system trust store. For private CAs:

ldap.ssl {
ca = "/etc/scribe/tls/ldap-ca.pem"
}

Never set insecure = true in production. It disables certificate verification.

Prevent locked or disabled accounts from authenticating:

auth.ldap {
filter = "(!(accountLocked=TRUE))"
}

For Active Directory:

auth.ldap {
filter = "(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
}

This filter excludes accounts with the “disabled” flag set.

Enable SSL for PostgreSQL connections:

Terminal window
SCRIBE_DATABASE_SSL_ENABLED=true

Or in HOCON:

database.ssl.enabled = true

Never commit database passwords to version control. Use environment variables:

Terminal window
SCRIBE_DATABASE_PASSWORD=your-password

Or reference external secret stores in your deployment configuration (Kubernetes secrets, Vault, etc.).

Don’t put passwords or API keys in config files that get committed to git. Use environment variables or a secrets manager instead.

IdentityScribe supports ${?ENV_VAR} substitution in HOCON configs:

auth.providers.default {
client-secret = ${SCRIBE_AUTH_CLIENT_SECRET}
}

If you use config files with secrets:

  • Set file permissions to 600 (owner read/write only)
  • Run IdentityScribe as a dedicated service account
  • Mount config files as read-only in containers

API schema endpoints reveal internal data structures. The default auto mode disables them in production while allowing access during development.

channels.rest.openapi.enabled = false

Or via environment variable:

Terminal window
SCRIBE_REST_OPENAPI_ENABLED=false

Affected endpoints: /api.json, /api.yaml, /openapi.json, /openapi.yaml

channels.graphql.introspection.enabled = false

Or via environment variable:

Terminal window
SCRIBE_GRAPHQL_INTROSPECTION_ENABLED=false

Affected endpoints: /graphql.json, /graphql.sdl, and introspection queries ({ __schema { ... } })

Both Scalar (REST) and GraphiQL (GraphQL) UIs also default to auto:

  • channels.rest.ui.enabled
  • channels.graphql.ui.enabled

Observe endpoints show operational state and can create support artifacts. Keep them authenticated and on an internal socket unless support explicitly asks for temporary local access.

monitoring.observe.auth.enabled = true
monitoring.mcp.auth.enabled = true
monitoring.observe.socket = "internal"
monitoring.prometheus.socket = "internal"

Use monitoring.observe.auth.rules to limit operator access. If you expose MCP over HTTP, keep monitoring.mcp.auth.enabled = true.

The redacted export is suitable for support handoff. The full export includes more deployment detail and should stay limited to trusted operators:

  • /observe/performance-insight/export
  • /observe/performance-insight/export/full

Both responses are sent without browser caching. Treat downloaded reports as support artifacts and store them with the same access controls as logs.

Recent Observe charts and performance exports need in-process metrics history and Prometheus scraping:

monitoring.prometheus.enabled = true
monitoring.stats-history.enabled = true

If charts show history as unavailable, check both keys and the socket used for Observe requests.

Heap dumps are full memory snapshots and can contain credentials or personal data. Keep capture disabled unless support asks for it:

monitoring.memory.heap-dump.enabled = false

When capture is enabled, set these keys before startup:

monitoring.memory.heap-dump.directory = "/var/lib/identity-scribe/heap-dumps"
monitoring.memory.heap-dump.max-files = 4
monitoring.memory.heap-dump.cooldown = 5m

The directory must be private to the service account and large enough for a full heap snapshot. Keep monitoring.memory.heap-dump.cooldown at several minutes or longer in production.

When IdentityScribe stops answering requests but the process is still up, a thread dump shows what every thread is waiting on. Send the quit signal to the running process:

Terminal window
kill -3 <pid>

The service writes the dump to its standard output and keeps running. Requests, health probes and synchronisation continue; nothing is restarted and no configuration change is needed.

Where the dump appears depends on how you run the service:

DeploymentWhere to read it
Linux system servicejournalctl -u identity-scribe --since "<start-time>"
Containerdocker logs <container> or kubectl logs <pod>

Take two or three dumps ten seconds apart. A thread that stands still across all of them is stuck; one that moves is merely busy. Attach the dumps to your support request — they contain thread names and code locations only, no directory entries, passwords or tokens, so they are far less sensitive than a heap dump.

Before deploying, verify each item:

  • Monitoring endpoints isolated from public network
  • Firewall rules block direct database access
  • TLS enabled on all public endpoints
  • auth.enabled = true
  • Access rules configured (deny-by-default is automatic)
  • Provider issuer URL points to your IdP
  • Audiences configured to match your tokens
  • Valid certificates installed
  • Certificate expiration monitored
  • Private keys protected with appropriate filesystem permissions
  • LDAPS or StartTLS enabled
  • Server certificate verified
  • Service account has minimal required permissions
  • Filter excludes locked/disabled accounts
  • No secrets in version control
  • Environment variables or secret manager for credentials
  • Config files have restricted permissions
  • channels.rest.openapi.enabled is auto or false
  • channels.rest.ui.enabled is auto or false
  • channels.graphql.introspection.enabled is auto or false
  • channels.graphql.ui.enabled is auto or false
  • monitoring.observe.auth.enabled = true
  • monitoring.mcp.auth.enabled = true if MCP is exposed over HTTP
  • Observe and Prometheus endpoints are on an internal socket
  • monitoring.stats-history.enabled = true when using Observe history charts or performance insight exports
  • monitoring.memory.heap-dump.enabled = false unless support requested a capture window
  • Heap dump directory, retention, and cooldown are configured before enabling capture
  • Logs don’t contain sensitive data (passwords, tokens)
  • Log files have appropriate retention and access controls