Security
Gestalt sits between callers and upstream APIs. It authenticates users, stores encrypted credentials, and isolates provider processes. For implementation details, see Architecture.
Trust boundaries
| Boundary | What crosses it | Protection |
|---|---|---|
| Internet to Gestalt | User requests | Platform auth (session or API token) |
| Gestalt to upstream APIs | OAuth tokens, API keys | Credential encryption, TLS, egress host allowlist |
| Gestalt to providers | Config, operation requests, access tokens | Process isolation, Unix socket, scoped capabilities |
| Operator to Gestalt | Config, encryption key, secrets | Secret manager, env var expansion, structured secret refs |
Encryption at rest
All plugin tokens are encrypted before storage using AES-256-GCM.
The server.encryptionKey is the root deployment secret. If the key is a 64-character hex string, it is decoded directly as a 32-byte AES key. Otherwise, Argon2id derives a 32-byte key from the string (3 iterations, 64 MiB, 4 threads).
Every encryption operation generates a fresh 12-byte nonce using crypto/rand. GCM provides authenticated encryption, so tampering with stored ciphertexts is detected at decryption time.
Access tokens, refresh tokens, pending connection state, and OAuth state parameters are all encrypted with AES-256-GCM. OAuth state carries a 10-minute TTL. API tokens are stored as one-way SHA-256 hashes. User emails and display names are stored in plaintext.
Token lifecycle
Platform sessions. Users log in through the configured identity provider. Gestalt validates the callback, checks email_verified, and issues a session cookie. Sessions expire based on the authentication provider’s TTL (default 24 hours).
API tokens. Generated with 32 random bytes from crypto/rand and prefixed with gst_api_. The plaintext is returned once at creation time. Only the SHA-256 hash is stored. Default TTL is 30 days, configurable via server.apiTokenTtl.
Plugin tokens (OAuth). OAuth state is encrypted with a 10-minute TTL to prevent state injection. After the authorization code exchange, access and refresh tokens are encrypted separately and stored. Gestalt automatically refreshes tokens that expire within 5 minutes.
Request authentication
Requests are authenticated in the following order:
- Check for a
session_tokencookie - Check for an
Authorization: Bearerheader with agst_api_prefix (API token lookup via SHA-256 hash) - Check for an
Authorization: Bearerheader with a provider-issued token - If no valid credentials are found, return 401
Security headers
| Header | Value |
|---|---|
Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Strict-Transport-Security | max-age=63072000; includeSubDomains (HTTPS only) |
Provider Isolation
Providers run as separate OS processes with communication over temporary Unix sockets. Providers cannot access the datastore, other users’ credentials, or the host process memory. Only sanitized environment variables are passed to provider processes.
The host resolves the caller’s access token and passes it to the provider in each gRPC request. Providers receive only the token for the current invocation.
Executable plugins may also opt into a runtime provider. In that model,
gestaltd first talks to the runtime provider, which creates a hosted session,
binds host-local services into that session, starts the plugin there, and
bridges the plugin’s provider gRPC listener back to the host. The effective
execution boundary then depends on the selected runtime backend and its
capabilities, not just the host-local child-process wrapper.
Egress control
Every provider entry supports egress.allowedHosts to declare which upstream
hosts it can reach. Provider-level allowedHosts is no longer accepted. When
server.egress.defaultAction is deny, providers without an explicit allowlist
are blocked from host-mediated outbound requests.
server:
egress:
defaultAction: deny
plugins:
my-plugin:
source: https://artifacts.example.com/plugin/my-plugin/v1.0.0/provider-release.yaml
egress:
allowedHosts:
- "api.example.com"
- "*.slack.com"Wildcards are supported (*.example.com matches api.example.com). The same
check applies uniformly to REST, GraphQL, and MCP providers.
OS-level sandbox
Executable plugin providers can run with additional OS-level isolation when
egress.allowedHosts is configured or when server.egress.defaultAction is
deny. That isolation currently depends on host-specific runtime support, so
it should not be treated as a portable security boundary by itself.
The built-in local runtime uses this same host-local sandbox path. Hosted
runtime providers may define a stronger or different execution boundary, but
they only count as equivalent for security policy purposes when their support
profile preserves the same policy. In particular, Gestalt’s
egress.allowedHosts model is hostname-based, so a backend that only supports
CIDR or firewall rules is not a drop-in replacement for hostname-aware egress
enforcement.
Filesystem. On Linux, Landlock restricts file access to system libraries, SSL certificates, and DNS configuration. Writes are limited to the socket directory and an isolated temp directory. On macOS, equivalent restrictions are applied via sandbox-exec.
Network. gestaltd starts a local HTTP proxy and sets HTTP_PROXY/HTTPS_PROXY in the provider’s environment. The proxy checks each request’s hostname against the same egress.allowedHosts and defaultAction rules used by all other provider types.
Process group. Sandboxed providers run in their own process group. On shutdown, gestaltd signals the entire group to ensure child processes are also cleaned up.
Operator responsibilities
| Setting | Impact |
|---|---|
server.encryptionKey | Root deployment secret. If compromised, all stored tokens can be decrypted. Use a strong random string or 64-character hex key. Must be the same across all instances in a cluster. |
providers.authentication | Omitting providers.authentication disables all authentication. Do not use in production. |
server.baseUrl | Must match the public URL exactly. OAuth callbacks are derived from it. |
| TLS termination | Gestalt does not terminate TLS. Deploy behind a reverse proxy that handles TLS. |
providers.secrets | Use a dedicated secret manager in production. env is acceptable but secrets may appear in process listings. |
providers.indexeddb and server.providers.indexeddb | Use a production-grade datastore provider for multi-replica deployments. SQLite is single-writer and not suitable for multi-replica deployments. |
Known limitations
Changing server.encryptionKey requires re-encrypting all stored tokens; there is no automated key rotation (see Security Internals: Key rotation). Platform sessions cannot be explicitly revoked and expire based on the configured TTL. Rate limiting and CORS must be configured at the reverse proxy layer. Gestalt encrypts tokens at the application level, but the database may store non-sensitive metadata in plaintext (connection metadata, scopes, timestamps). See Data Model: Encryption boundaries for the full list. Appropriate database access controls are still important.