For the public mapping of assets, trust boundaries, threats, controls, and residual risks, see the threat model. The OAuth and Console security design delta records the implemented remote-OAuth and local Console control-plane gates; this page is the detailed operating-security reference for implemented controls.
Miftah is a credential broker, so safe defaults are part of the product contract:
- credentials stay local and telemetry is disabled;
- plaintext secret references are disabled unless explicitly enabled;
- environment values are injected into child processes and never returned by management tools;
- upstream stderr, errors, diagnostics, audit entries, and tool results pass through redaction;
- profile switching can be disabled or locked to a single profile;
- destructive and ambiguous requests are not silently routed;
- audit records contain metadata, not sensitive payloads or arguments, by default;
- audit files and directories are owner-only where platform support permits it, and audit-write failures are explicit;
- audit rotation retains only managed, single-link regular archive files with stable identities and does not follow symlinks or externally linked paths outside its configured directory;
- audit export is explicit and local-only; it applies redaction again and omits stored arguments unless an operator opts in;
- durable active-profile state is opt-in, uses derived owner-restricted paths, and stores no credentials;
- the optional Streamable HTTP server binds literal loopback by default and bounds sessions and request bodies before runtime allocation;
- the separately launched Console control API binds only literal loopback, requires one-use bootstrap, exact Host/Origin, short browser sessions, and CSRF for mutations;
- MCP tool annotations are ignored for risk downgrades unless the operator explicitly trusts the configured upstream that supplied them;
- provider tokens should be separate, least-privilege tokens per account and risk level.
External secret providers execute only fixed programs with argument arrays and bounded stdout/stderr capture. Miftah does not expose provider output in an error, audit record, health entry, or doctor report. On Windows it resolves provider executables without current-directory lookup and uses a static System32 PowerShell launcher that joins a JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE Job Object before creating the provider. Timeout, cancellation, output-limit, or launcher termination closes that job and terminates ordinary provider descendants as well. This process-tree guarantee does not cover providers that intentionally escape through services, scheduled tasks, elevation brokers, or WMI process creation.
Windows STDIO upstreams are resolved before the MCP SDK starts a child process. Miftah passes the SDK only an absolute direct .exe or .com path resolved from the configured command and final profile environment; it refuses .cmd/.bat shims and shell executables such as cmd.exe, PowerShell, and pwsh rather than relying on the SDK's Windows command-processor fallback. npm's npx runner itself uses a Windows command shell, so the generic, sentry, and generic-npx presets are unavailable on Windows. This does not remove direct Windows support for a reviewed local executable, docker.exe, uvx.exe, or other direct executables discoverable from an absolute PATH entry.
Allowlisted local plugins use the same bounded argument-array child-process path and Windows Job Object containment. Miftah preflights their API manifest before serving, verifies the resolved module remains below the configuration directory, scrubs the child environment, and gives secret providers only one canonical reference and routing matchers only a tool name plus bounded canonical signals. This isolates ordinary plugin failures and withholds Miftah's resolved configuration; it is not an operating-system sandbox for an arbitrary same-user Node module. A plugin can still make its own filesystem, process, or network calls, including actions affecting its parent or host, so every configured module and changes made after preflight must be reviewed and trusted by the operator. See local plugins.
Profile credential isolation is a separate, opt-in boundary. On macOS and Linux it gives one profile/upstream target a marker-owned, owner-restricted runtime tree and copies only explicitly configured files from below the canonical configuration directory. Miftah never resolves, materializes, injects, or bind-mounts another profile's managed path. It does not make a hostile native STDIO process safe: a process running under the same OS user can still open another profile's absolute path. Stronger native containment needs a separate OS identity or a correctly configured OS sandbox.
Docker/Podman can provide a stronger file boundary only when the container receives the intended generated bind mounts and no other host directories that expose profile data. Read-only mounts are the default; an explicit writable mount weakens that boundary. Miftah rejects conflicting user mount/environment flags rather than attempting to merge them, rejects explicit remote engine endpoints, contexts, and config-location overrides, and fails closed for Podman isolation on macOS. The default local daemon remains trusted operator infrastructure. Miftah rechecks managed paths before it returns argv, but a hostile same-user process can still race a path before Docker/Podman resolves it; argument arrays cannot provide an atomic path-to-daemon handoff. It treats stderr from an isolated child as sensitive and emits only a fixed redaction marker.
Windows profile credential isolation fails closed before it creates or copies a runtime file. Node's POSIX-style mode API does not establish or verify a restrictive Windows DACL, so pretending that 0600/0700 protects a Windows credential would be unsafe. This limitation does not affect Windows secret-provider process containment.
miftah serve --transport http is loopback-first: it binds only literal 127.0.0.1 by default. ::1 is also a literal-loopback option. Every other bind value, including localhost, a hostname, or 0.0.0.0, requires an explicit allowNonLoopback: true, a secret-backed bearer authToken, and an exact allowedHosts list. Miftah emits one fixed nonloopback warning without the token or resolved provider output.
The server rejects duplicate or malformed Host, Origin, authorization, and MCP-session headers. It compares one Authorization: Bearer value to the resolved token in constant time, never places the token in a response or diagnostic, and does not use cookies. Browser requests are denied by default: an absent Origin is valid for MCP clients, but a supplied origin must exactly match an explicit allowedOrigins entry. Miftah does not emit permissive CORS headers.
Before it allocates a session, the host validates the exact /mcp endpoint, method, Host, Origin, authentication, JSON content type, body size, and one initialize request. For modern requests, the SDK additionally validates the required Mcp-Method and operation-specific Mcp-Name mirrors against the parsed JSON-RPC body before the request-scoped broker can route, authorize, meter, audit, or execute it. Miftah rejects the unsupported Mcp-Param-* extension with a fixed response and strips its schema keyword from modern HTTP catalogs, so parameter names and values cannot cross into policy, audit, diagnostics, or an upstream. Session capacity includes in-progress initializations and closing runtimes. Each session receives a distinct Miftah runtime, profile/session state, approval/lock/lease/routing state, and upstream manager. Idle expiry, DELETE, and graceful shutdown detach the session immediately and close its runtime, which closes retained upstream transports and their ordinary descendants; detached cleanup retains capacity until it succeeds, and a failure remains capacity-consuming and is reported. An HTTP reconnect reuses only its existing session ID; it cannot create or access another client's runtime.
Modern cacheable results are private with a zero TTL. Miftah deliberately enables no positive-TTL shared cache until a cache can be keyed by authenticated principal plus profile, policy, and configuration revision and invalidated atomically on every relevant change. This prevents catalog reuse across accounts and makes profile or policy changes visible on the next request.
The Console control plane is not this MCP HTTP server. miftah dashboard or the API-only miftah console command creates a separate listener with no /mcp route, MCP transport, upstream session registry, or MCP bearer. It requires the exact listener Host for all traffic. State-changing browser requests require the exact listener Origin, including scheme, host, and port, plus in-memory CSRF proof; authenticated GET/HEAD reads may omit Origin because normal same-origin browsers do not consistently send it. Its CSPRNG bootstrap code appears only in the launching terminal, expires after five minutes, and is consumed once; the resulting opaque HttpOnly same-site session and in-memory CSRF proof are bounded by idle and absolute lifetimes. Every durable mutation prepares a separate fail-closed owner-restricted Console audit journal first. Browser responses contain allowlisted metadata and redacted status only. See the Console API contract.
Audit writes default to fail-closed: Miftah verifies the configured sink before dispatch and refuses a request when the sink cannot be prepared. A terminal write can fail after an upstream side effect has completed, so a post-dispatch AUDIT_WRITE_FAILED has an indeterminate outcome and must not prompt a blind retry of a non-idempotent operation. An operator can set audit.failureMode to "fail-open" for availability-sensitive deployments; Miftah then preserves the request outcome but exposes a redacted AUDIT_WRITE_FAILED health entry. This mode trades complete auditability for availability.
When configured, audit rotation occurs only at completed JSONL batch boundaries and retention acts only on Miftah-managed, single-link regular archive names with stable file identities within the configured directory. Its kernel-released coordination is local to one host, so a managed journal must not be concurrently shared across machines through a network filesystem. The optional sha256-chain integrity mode hashes already-redacted records and tracks the retained segment set so audit-verify can identify the first safe broken record. It is tamper evidence, not a cryptographic signature, nonrepudiation mechanism, or remotely anchored immutable log: a party able to replace every local journal and its integrity metadata can defeat the evidence. Preserve compliance or incident evidence in an independently protected destination. audit-export is a user-invoked local transformation, not telemetry; it repeats redaction and drops stored arguments unless --include-arguments is explicitly requested.
The default security.approvalMode is "human": a confirmation-required request receives a generic MCP form only from a form-capable client, and a client without that capability fails closed without an approval bearer. An approval bearer exists only when an operator explicitly selects "delegated-agent" for automation. It is a short-lived capability for one pending, exact MCP operation, not a credential or proof of a human identity. Miftah binds it to the connection and target context, stores only keyed digests, and invalidates it on denial, consumption, expiry, or a new connection. Do not copy a delegated bearer into logs, tickets, or another connection; approval audit events deliberately omit it and full operation arguments while recording whether the approval mechanism was form or delegated-agent.
Active-profile state is configuration-owned: MCP callers can select a profile but never a scope or storage path. workspace and global storage require explicit opt-in, derive a config-identity-namespaced location, and reject arbitrary state paths. A record contains only a format version, scope, config identity, selected profile, and timestamp; it never contains a secret, raw config path, provider output, or other MCP request data. Miftah writes it through a synced temporary file and atomic rename, applies owner-only permissions where supported, and reports a safe write failure without changing the in-memory selection. A configured profile lock wins over stored state; corrupt, stale, or unreadable state falls back safely and exposes only a stable diagnostic code.
Runtime profile locks and leases are connection-bound controls, not credentials or identity proof. A runtime lock or confirmation does not authenticate a human, and a lease never grants access to a differently routed profile. Miftah clears runtime locks and leases at a new connection boundary and never writes them to durable profile state. security.lockToProfile is operator-controlled and stronger than the optional MCP lock tools; clients cannot remove it. When fail-closed dedicated profile audit recording fails, Miftah rolls back the related profile mutation rather than retaining an unaudited switch, lock, or unlock.
The explicit runtime configuration is trusted operator input; workspace routing metadata is not. Project markers cannot inject configuration because their only accepted shape maps the configured wrapper name to an already-known profile. They cannot add credentials, environment variables, headers, upstreams, policies, audit controls, or secret references. Miftah reads only named, bounded metadata files within the applicable working-directory/root boundary and does not scan arbitrary project content.
MCP tools/list annotations are behavioral hints supplied by the upstream, not proof of safety. Miftah ignores them by default. An operator may set trustToolAnnotations: true only on the exact base upstream declaration they trust; a profile override cannot change that decision. Even for a trusted upstream, missing or contradictory hints never reduce risk, and idempotentHint/openWorldHint never lower it. Miftah records only the resulting classification source and confidence in route preview and audit data, never raw annotation objects. Separately, the built-in trusted-command-adapter classification is limited to the canonical official PostHog endpoint https://mcp.posthog.com/mcp and its bounded command grammar; it does not trust arbitrary exec names, alternate URLs, local processes, provider descriptions, or raw command text. Invalid or unrecognized command forms remain destructive.
Routing evidence is deliberately narrower than routing context. It is passed through audit redaction before it reaches a client or JSONL record, strips URI userinfo/fragments and redacts URI query values, and never contains the raw MIFTAH_PROJECT value or arbitrary project file content. Fixed provider matchers receive only bounded top-level argument fields, canonical provider identifiers, and a separately normalized GitHub repository context; allowlisted routing plugins receive only a tool name and the same bounded canonical signal projection through a scrubbed child host. Neither receives nested arguments, the raw routing context, a raw provider URL, or secret-bearing URI components. routingMatcherEvidence records only a selected profile plus canonical provider/kind/value identifiers, and is structurally validated before an ambiguity is audited. An unrecognized environment profile, ambiguous matching rules, or ambiguous matcher bindings fails closed instead of selecting an account; standard project-marker discovery deterministically uses the nearest valid marker. Profile hints and matcher reasons also cannot satisfy an explicit-rule requirement for destructive operations.
Miftah cannot reduce privileges granted by a provider token. A read-only Miftah policy is a local blocklist, not a replacement for provider-side scopes. Avoid putting real credentials in examples, commits, or support logs.
init never asks for or echoes a secret value: its wizard accepts variable names and validated endpoint/header metadata only. Generated provider examples contain ${ENV_NAME} references, and client snippets are printed for explicit copy rather than written into host configuration files. For exact preset pins, GitHub tag-to-digest recording guidance, Sentry skill-filtering limits, and client locations, see preset and client compatibility.
The STDIO transport is the default because it avoids a network listener. Remote upstream connections carry profile-bound credentials, so Miftah requires HTTPS outside loopback development URLs (localhost, 127.0.0.0/8, or ::1). It uses Node's normal certificate validation and does not disable TLS verification; self-signed endpoints fail closed unless an operator explicitly provides a trusted local CA through Node's normal trust configuration. Do not use NODE_TLS_REJECT_UNAUTHORIZED=0, and do not send credentials to a cleartext non-loopback endpoint.
For remote HTTP diagnostics, Miftah retains only a stable HTTP status or MCP protocol code. It deliberately omits server response bodies and remote protocol messages, because they can contain credentials or sensitive provider context. Streamable HTTP is preferred and sends DELETE on intentional local session cleanup; legacy SSE is deprecated and has no equivalent remote-session deletion. The local HTTP server follows the same lifecycle rule for each isolated session and adds loopback-first binding, explicit nonloopback authentication, Host validation, Origin denial by default, and bounded cleanup.
Miftah runs a narrow standards-compatible OAuth client only for exact version-3 HTTPS Streamable HTTP bindings. It validates protected-resource and authorization-server metadata, requires authorization code plus PKCE S256 and RFC 9207 issuer response support, uses a single-use literal-loopback callback, stores credentials through the OS vault, and refreshes within the same configuration/profile/upstream/resource/issuer binding. It can also pass explicit secret-backed headers or launch an upstream that manages its own authentication under the ordinary configuration contract, but it does not own, parse, scrape, import, replay, or lifecycle-manage provider passwords, browser cookies, or arbitrary third-party token caches as OAuth artifacts. OAuth authorization URLs, codes, callback parameters, tokens, client secrets, and raw provider errors must never enter configuration, audit records, or diagnostics. See OAuth support for supported classes and remaining lifecycle boundaries.
Identity verification is an optional, local account-fingerprint comparison. It is not credential validity, provider authentication, account authorization, or scope validation. Miftah does not ship provider SDKs or plugins for it.
The live-verification statuses are unconfigured, not-verified, verified, expired, mismatch, unsupported, and failed. The separate durable binding states are verified, unverified, changed, expired, and unavailable. Management status retains only safe profile/upstream names, configured expected and allowed actual/bound fingerprint fields (provider, accountId, login, organization, host), verification times, stable error codes, and a bounded unsupported-capability diagnostic. That diagnostic contains only the configured tool name, the read-only-no-required-input requirement, and a fixed reason; it never includes a discovered schema or upstream response. An accountId must use the exact non-email opaque grammar before it can enter status, audit, or persistence. Audit output records the binding state and live safe result but omits the duplicated persisted bound evidence. All values traverse normal redaction before they cross either boundary.
Live identity verification is process/session-generation-bound by maxAgeMs and the exact profile/upstream target. A restart, crash, or idle session replacement therefore requires another live probe before protected work. Separately, an opted-in probe persists only its configured allowlisted fields, exact target, timestamp, and hashed configuration identities in an atomic owner-restricted state file. It does not persist a raw response, unconfigured field, raw account payload, tool argument, error body, arbitrary JSON, credential, token, or raw configuration path. Doctor never reports raw identity output or fingerprint values.
Before parsing or normalization, identity verification accepts exactly one MCP text content item with at most 4,096 JavaScript characters. Any other response shape or longer text fails safely; JSON probes accept only an object and retain only validated allowlisted fingerprint strings.
Required checks fail closed for their configured write or destructive risks: a changed/mismatched fingerprint, unavailable binding store, unsupported probe, or failed verification blocks that protected operation with a stable diagnostic such as IDENTITY_MISMATCH or IDENTITY_BINDING_UNAVAILABLE. Optional identity configuration is observational and does not gate operations. In multi-profile configurations, opt-in explicit and confirmed selection modes additionally require the requesting client's captured current-session choice; a durable change from another process cannot authorize or silently switch that active client. See identity verification configuration for the probe and response contract.