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:
export MCP_VENDOR_ENVIRONMENT=qaexport 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.
Modes and fallback
Section titled “Modes and fallback”| 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.
Complete credential sets
Section titled “Complete credential sets”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.
NinjaOne authentication bundles
Section titled “NinjaOne authentication bundles”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-levelNINJAONE_URL; omit the tool’sserverargument. The alias namedefaultis reserved when using targeted rules.server/qa5: theqa5entry inNINJAONE_SERVERS.database/qa5/central: central queries and division discovery inqa5.database/qa5/division/host1: division queries for the allowlistedhost1key. The tool uses its existingdbHostanddbNamearguments. 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.
Architecture
Section titled “Architecture”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.
Lifecycle, evidence, and explanations
Section titled “Lifecycle, evidence, and explanations”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.
Shared-call attribution
Section titled “Shared-call attribution”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.