Skip to content

Per-user credential selection

The gateway can select an existing vault credential for the authenticated caller, then use a shared credential when that caller has no mapping. Vault access remains read-only: provision tokens with the provider’s UI, CLI, or deployment automation. NinjaOne login bundles can acquire sessions using the existing native-account OTP flow. OAuth consent, credential enrollment, and vault writes remain separate work.

Configure MCP_CREDENTIAL_SELECTION as a JSON string in the process environment, .env, or the shared section of ~/.mcp/configs.json. For example:

Terminal window
export MCP_VENDOR_ENVIRONMENT=qa
export SLACK_TOKEN='vault://secret/mcp/shared/slack#token'
export MCP_CREDENTIAL_SELECTION='{
"rules": [
{
"vendor": "slack",
"environment": "qa",
"mode": "user_then_shared",
"users": [
{
"issuer": "https://identity.example/realms/acme",
"tenant": "acme",
"subject": "immutable-idp-subject-alice",
"credentials": {
"SLACK_TOKEN": "vault://secret/mcp/users/alice/slack#token"
}
}
]
}
]
}'

Configure the provider connection separately as described in the secret sources runbook. References can use file://, vault:// (Vault/OpenBao), awssm:// (AWS Secrets Manager), or azkv:// (Azure Key Vault), including different providers in the same deployment.

Mode User mapping present No user mapping
shared_only User mappings are not permitted in this rule Select the existing shared credential
user_then_shared Select that user’s credential Select the shared credential
user_required Select that user’s credential Refuse the call

Vendors with no selection rules keep existing behavior. Once a vendor has rules, every environment in which it is used must have an explicit rule. An unclassified or unmatched environment is refused; it never downgrades a user_required rule. Rules accept prod, staging, qa, or dev. Flat mappings use the vendor’s configured MCP_VENDOR_ENVIRONMENT; targeted bundles use the classification on the exact matching target rule. A target has one classification. Database targets must match their QA/dev alias.

Lookup uses the exact verified issuer + tenant + subject from inbound authentication. Use the issuer spelling reported by the gateway, without adding a trailing slash. Group membership, email addresses, client-reported identity, and tool arguments cannot supply an account identity. They may select an administrator-configured target, whose rule is then evaluated for that caller. Local/unauthenticated mode has no user mapping; it can use shared fallback but cannot satisfy user_required.

A mapping that exists but has an unreadable, missing, empty, or rejected credential never falls back to shared. No shared credential means the call is refused. The selected account is authorized by policy before execution; permission to use a personal account does not imply permission to use a shared account.

Each mapping contains exactly one registered vendor secret and all of its account fields. Secret values must be provider references; account identifiers may be references or literal strings. User mappings cannot use OS-keychain sentinels. Existing shared credentials continue to support the OS keychain.

For Jira or Confluence, supply both ATLASSIAN_USER_EMAIL and ATLASSIAN_API_TOKEN. For Bitbucket, choose that pair or ATLASSIAN_BITBUCKET_USERNAME + ATLASSIAN_BITBUCKET_APP_PASSWORD, not both. For Zoom, supply ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, and ZOOM_CLIENT_SECRET. For Mend, supply MEND_EMAIL, MEND_ORG_UUID, and MEND_USER_KEY. For WRDS, supply WRDS_USERNAME and WRDS_PASSWORD. Single-token vendors need only their registered token key, for example SLACK_TOKEN or GRAFANA_TOKEN.

Put multipart credentials in one provider document and reference its fields, so rotation remains atomic. All alternate shared secret and account fields are masked for the selected call, including implicit keychain fallback. Hosts, tool settings, and resource permissions remain administrator-controlled; mappings cannot override arbitrary configuration keys.

NinjaOne flat credentials mappings retain static-only behavior: one NINJAONE_ACCESS_TOKEN, NINJAONE_SESSION_KEY, or NINJAONE_SESSION_COOKIE against the top-level URL. Use the targeted bundles below for named servers, automated login, and databases. A vendor cannot mix flat and targeted rules; convert its existing default mapping to server/default when adding targets.

An administrator is responsible for assigning a vendor credential to its owner. The gateway verifies the inbound user’s identity, but does not prove that a provisioned API token belongs to that same person at the vendor. authority: delegated means an explicitly assigned per-user credential here; it does not assert that OAuth consent occurred. Zoom S2S credentials still represent their upstream application account even when assigned to one gateway user.

Each targeted rule selects one complete bundle. Configure destinations in NINJAONE_SERVERS / NINJAONE_URL or NINJAONE_DB_ENVIRONMENTS; bundles contain only account inputs. A vault document can hold all fields for an account, with each bundle field referencing that document. The same syntax works for every configured secret provider.

Bundle type Fields
access_token token
session_key session_key
session_cookie cookie
ninjaone_login email, password, optional totp_secret
ninjaone_database username, password

All secret fields, including those in a shared bundle, must be provider references. Email/username may be literals or references. Unknown fields, mixed auth methods, literal secrets, keychain sentinels, and executable commands are rejected. Omitted TOTP inputs do not inherit a shared seed or command. Legacy administrator-configured OTP commands remain available outside selection rules; targeted bundles use a seed or the login tool’s explicit MFA code.

Example MCP_CREDENTIAL_SELECTION value:

{
"rules": [
{
"vendor": "ninjaone",
"environment": "qa",
"target": "server/qa5",
"mode": "user_then_shared",
"shared": {
"type": "ninjaone_login",
"email": "shared@example.com",
"password": "vault://secret/mcp/shared/ninjaone#password",
"totp_secret": "vault://secret/mcp/shared/ninjaone#totp_secret"
},
"users": [
{
"issuer": "https://identity.example/realms/acme",
"tenant": "acme",
"subject": "immutable-idp-subject-alice",
"bundle": {
"type": "ninjaone_login",
"email": "vault://secret/mcp/users/alice/ninjaone#email",
"password": "vault://secret/mcp/users/alice/ninjaone#password",
"totp_secret": "vault://secret/mcp/users/alice/ninjaone#totp_secret"
}
}
]
}
]
}

Pair this with NINJAONE_SERVERS={"qa5":{"url":"https://your-console.example"}}. Call ninjaone_get with server: "qa5". The application selects the bundle, authorizes/journals it, then uses a cached session or performs native OTP login. Existing credentials embedded in that server entry are ignored for selected calls. The caller cannot change the destination by changing vault contents.

Targets are exact administrator-owned keys:

  • server/default: top-level NINJAONE_URL; omit the tool’s server argument. The alias name default is reserved when using targeted rules.
  • server/qa5: the qa5 entry in NINJAONE_SERVERS.
  • database/qa5/central: central queries and division discovery in qa5.
  • database/qa5/division/host1: division queries for the allowlisted host1 key. The tool uses its existing dbHost and dbName arguments. An omitted host is accepted only when the environment has exactly one configured division host.

A missing target rule is an error. Shared fallback applies only to an absent user mapping within the matching target rule. Define a shared bundle explicitly when fallback is wanted; the application never borrows embedded JSON credentials. user_required rules need no shared bundle. shared_only rules have no users. For database targets use, for example:

{
"type": "ninjaone_database",
"username": "vault://secret/mcp/users/alice/ninjaone-db#username",
"password": "vault://secret/mcp/users/alice/ninjaone-db#password"
}

Additional named connections use targets such as database/qa5/named/lockhart.governor and database/qa5/named/lockhart.shard. The last component selects a nested lockhart connection or a key in the environment’s databases map.

With selected DB bundles, NINJAONE_DB_ENVIRONMENTS can contain destinations without usernames/passwords. Its existing host, database-name, QA/dev, TLS, read-only SQL, and row-limit constraints remain enforced. Central, division, and named database accounts are selected separately, and every query opens a fresh authenticated connection. Database bundles cannot authenticate HTTP tools or vice versa. The ninjaone-db build feature is required for database tools.

Login sessions are memory-only, bounded to 1,024 partitions, and reused for at most 15 minutes before a fresh login. Partitions include verified caller identity (even for shared accounts), selected target, actual URL, and the bundle’s values and provider versions. Rotation retires the previous partition for new calls; in-flight calls retain their pinned snapshot. Concurrent calls for one partition share a login, and OTP generation occurs after acquiring that partition’s lock. Idle partitions may be evicted at capacity; active logins are never evicted.

A rejected session is evicted and the current request returns an error. The next call can log in again using the same selected account; mutating requests are not replayed. A delayed rejection cannot evict a newer, different session. Explicit ninjaone_login forces a fresh login and supports a caller-provided MFA code or reCAPTCHA token. Federated SSO and unattended CAPTCHA handling are not added. If no TOTP seed is provisioned, MFA may require the explicit login tool.

auth/bundles.rs validates typed account inputs. auth/selection.rs orchestrates selection through the existing CredentialBroker boundary and the new CredentialTargetResolver port; the NinjaOne target adapter resolves allowlisted destinations. Secret references continue through SecretSource adapters and the existing immutable snapshot/refresh pipeline. The MCP adapter passes the selected snapshot into application/calls.rs::CallService for admission. Policy, intent, outcome, and dispatch retain that same credential identity; selection failures are refused even when no audit journal is configured.

auth/login_session.rs owns session reuse, expiry, and login coordination through SessionAuthenticator and SessionStore ports. The NinjaOne HTTP/OTP adapter implements authentication; MemorySessionStore implements bounded storage. Tests substitute both ports. The PostgreSQL adapter receives the selected account after destination validation. MCP tools remain thin controller entry points, and their schemas/descriptions are unchanged.

Mappings are compiled once per configuration snapshot. Startup and global-config reload validate the rules and resolve their references before serving them. Malformed reloads keep the previous configuration. Provider refresh failures keep the last good secrets and degrade health, as with existing shared references; they never switch accounts. An upstream 401 can request an early refresh of the same reference. A call pins its selected credential values, account, and provenance through policy evaluation, audit, and execution, even if configuration rotates.

Enterprise audit events and response metadata report authority: delegated or shared, plus selection: user_mapping_present, user_mapping_absent, or shared_only. Per-user slot labels include a digest of the verified identity and, for bundles, the selected target. Provider source/version are retained. No token values are emitted. Selection failures produce a denied intent with reason credential_selection_failed.

mcp-devtools policy explain ... --subject <sub> --tenant <tenant> and the admin policy-explain endpoint use the same selection function and target resolver. Supply the tool arguments (for example server or environment/dbHost/dbName) to explain a targeted call. The CLI derives the issuer from configured inbound authentication. --environment selects the rule for that environment as well as the policy context. CLI explanation resolves configured secret references, so the operator needs provider read access.

Token and response caches distinguish credential identity. Artifact/session owners also include the verified issuer; existing in-memory owners disappear on restart. The Bitbucket default-workspace cache is bypassed for explicit selection because its existing single slot is shared by the whole server.

Documents are limited to 4 MiB, 256 rules, and 10,000 total user mappings. Startup and background refresh run at most 16 provider reads at once. Duplicate rules/users, unknown fields/modes, and incomplete credentials are refused without echoing document content in errors.

Local validation and allocation evidence are recorded in the selection benchmark report and the targeted bundle report.

Calls without a matching selection rule also prepare their credentials before an audited dispatch. See credential preparation for rotation behavior, NinjaOne cached-session attribution, and the distinction between diagnostic predictions and execution evidence.