Skip to Content
Security

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

BoundaryWhat crosses itProtection
Internet to GestaltUser requestsPlatform auth (session or API token)
Gestalt to upstream APIsOAuth tokens, API keysCredential encryption, TLS, egress host allowlist
Gestalt to providersConfig, operation requests, access tokensProcess isolation, Unix socket, scoped capabilities
Operator to GestaltConfig, encryption key, secretsSecret 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:

  1. Check for a session_token cookie
  2. Check for an Authorization: Bearer header with a gst_api_ prefix (API token lookup via SHA-256 hash)
  3. Check for an Authorization: Bearer header with a provider-issued token
  4. If no valid credentials are found, return 401

Security headers

HeaderValue
Content-Security-Policydefault-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-Optionsnosniff
X-Frame-OptionsDENY
Strict-Transport-Securitymax-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

SettingImpact
server.encryptionKeyRoot 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.authenticationOmitting providers.authentication disables all authentication. Do not use in production.
server.baseUrlMust match the public URL exactly. OAuth callbacks are derived from it.
TLS terminationGestalt does not terminate TLS. Deploy behind a reverse proxy that handles TLS.
providers.secretsUse a dedicated secret manager in production. env is acceptable but secrets may appear in process listings.
providers.indexeddb and server.providers.indexeddbUse 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.