feat(pam launch): add just-in-time (JIT) access via -j/--jit (ephemeral + elevation)#1979
Conversation
67b6ba6 to
507fb85
Compare
74e9306 to
43e36d7
Compare
|
@sergeyk — both issues addressed and force-pushed to Python 3.7 breakage — the JIT helpers ( Merge conflicts — rebased onto current CI now green on both 3.7 and 3.12. PR is a single clean commit on top of |
43e36d7 to
c11930c
Compare
Adds opt-in JIT provisioning to `pam launch` in both flavours supported by the Gateway: - Ephemeral account (create_ephemeral=true): Gateway creates a short-lived account on the target for the session and deletes it on disconnect. - Privilege elevation (elevate=true): Gateway adds the linked credential to an elevated group/role for the session and reverts on disconnect. - Both combined: ephemeral account provisioned *and* immediately elevated. When `-j/--jit` is passed, Commander emits `credentialType: 'ephemeral'` to the Gateway (for ephemeral/both) alongside optional `jitSettings` / `jitElevation` payloads. In ephemeral mode the guacd connection leaves username/password empty so the Gateway's server-side credential injection wins. Two authoring paths are supported uniformly: 1. Web Vault UI (authoritative) — encrypted DATA edge with path `jit_settings` on the resource vertex in the DAG. Keys are camelCase (createEphemeral, elevate, elevationMethod, elevationString, baseDistinguishedName, ephemeralAccountType), matching DagJitSettingsObject.to_dag_dict. 2. Declarative (`pam env apply`) — snake_case block under pamSettings.options.jit_settings on the record's typed field. All JIT shape handling (loader, normaliser, mode derivation, payload projection, credential-check predicate) is centralised in keepercommander/commands/pam_launch/jit.py so the wire contract has exactly one place to change if the Gateway flips casing or adds a field. `launch.py` and `terminal_connection.py` import the public API from that module directly; no JIT logic is duplicated. Precedence matches Web Vault: allowSupplyHost > JIT > allowSupplyUser > linked. `-j` is rejected on records with allowSupplyHost and is mutually exclusive with `-cr/-H/-hr`. Credential requirements follow the mode: - Ephemeral / both: Gateway provisions the account, so the pre-flight linked-credential check is bypassed (jit.provisions_credential predicate). - Elevation: linked credential is still required; a clear error is raised if the record has none. - Domain ephemeral (ephemeralAccountType=domain) binds to a pamDirectory via a separate DAG LINK edge (path="domain"); the Gateway owns that validation — Commander does not re-check it. JIT is strictly opt-in: records that happen to have jit_settings never auto-trigger JIT unless `-j` is passed, preserving backward compatibility. Tested: - 32 unit tests in unit-tests/pam/test_pam_launch_jit.py covering the normaliser (camelCase/snake_case/collision/unknown-key), mode derivation, payload builders (key isolation, empty-value dropping, disjointness), the DAG-first loader, and the credential-bypass predicate. - All 135 PAM unit tests pass. - End-to-end validation against the Acme lab (Lab Rocky pamMachine) exercised all three JIT modes (ephemeral, elevation, both) by writing camelCase JIT settings to the DAG (mirroring Web Vault), then calling detect_protocol, extract_terminal_settings, and the jit payload builders to verify the `credentialType` and `jitSettings`/`jitElevation` dict Commander POSTs to the Gateway. Lab state snapshotted and restored in a try/finally. - Reproduced `pam launch --jit` on a freshly gateway-linked lab record with no linked credential (the failure mode Michael reported) and confirmed the credential pre-flight is now bypassed for ephemeral/both. Made-with: Cursor
c11930c to
bd12de1
Compare
Summary
Adds opt-in just-in-time (JIT) provisioning to
pam launch, covering bothflavors defined by the keeper-pam-declarative schema:
create_ephemeral: true) — Gateway creates a short-livedaccount on the target for the session and deletes it on disconnect.
elevate: true) — Gateway adds the linked credentialto an elevated group/role for the session and reverts on disconnect.
Commander now reads
pamSettings.options.jit_settingsoff thepamMachinerecordand, when the operator passes
-j/--jit, emits a newcredentialType='ephemeral'to the Gateway (for ephemeral/both) alongside optional
jitSettingsandjitElevationpayloads that mirror the declarative schema keys verbatim. Theguacd connection settings leave username/password empty in ephemeral mode so the
Gateway's server-side credential injection wins.
This replaces the
# TODO: Add JITplaceholder left inkeepercommander/commands/pam_launch/launch.py.Motivation
The Keeper PAM declarative layer and pam-environment JSON schema already define
pamSettings.options.jit_settings(seekeeper-pam-declarative/manifests/pam-environment.v1.schema.json$defs.jit_settings), and the Gateway already supports the ephemeral credentialtype. The missing piece was the Commander client: it previously ignored
jit_settingsentirely and thepam launchcommand had an explicit# TODO: Add JITcomment. This PR wires it end-to-end with no changes to the Gateway.Precedence (matches Web Vault)
allowSupplyHost > JIT > allowSupplyUser > linked:-jis rejected on records withallowSupplyHost(clear error).-jis mutually exclusive with-cr,-H,-hr— JIT provisions thecredential itself.
ephemeral_account_type=='domain'requirespam_directory_uid_refon therecord, matching the declarative validator.
jit_settingsnever auto-trigger JIT unless-jis passed — strictly opt-in, fully backward compatible.
Wire contract
The gateway inputs dict gains three optional keys:
credentialTypeephemeraljitSettings = {create_ephemeral, ephemeral_account_type, base_distinguished_name?, pam_directory_uid_ref?}linked+userRecordUidjitElevation = {elevate, elevation_method, elevation_string}ephemeraljitSettingsandjitElevationKey names mirror the declarative schema verbatim (
_JIT_EPHEMERAL_KEYS/_JIT_ELEVATION_KEYS) so adapting to any future camelCase switch on the Gatewayside is a one-function edit to the two builder helpers in
terminal_connection.py.Files changed
keepercommander/commands/pam_launch/launch.py—--jit / -jflag,validation,
_get_jit_settings,_derive_jit_mode; removes the# TODO: Add JITcomment.keepercommander/commands/pam_launch/terminal_connection.py— readjit_settingsinextract_terminal_settings; threadjit_enabled/jit_modethrough
create_connection_context; dispatch newcredentialType='ephemeral'branch in the offer builder; leave guacd creds empty when ephemeral;
_build_jit_ephemeral_payload/_build_jit_elevation_payloadprojectors.keepercommander/commands/pam_launch/README.md— new, documents the flag,modes, precedence, and gateway compatibility.
unit-tests/pam/test_pam_launch_jit.py— 20 new unit tests.Usage
Record shape:
Test plan
unit-tests/pam/test_pam_launch_jit.pycoveringpayload builders,
jit_modederivation, record extraction, and theephemeral/elevation disjointness invariant.
pytest unit-tests/pam/) — 123 passed (103 pre-existingrelease.ReadLintsclean on all edited files.Lab Rocky — Terraform Training (SSH),UID
CzYUVX3cW3YOkEu2Jt4bQA): temporarily flippedpamSettings.options.jit_settingsthrough all three modes and captured theexact
inputsdict Commander would POST to the Gateway before short-circuiting,then restored the original record state in a try/finally. Captured payloads
below.
Captured wire payloads (live acme lab)
Scenario 1 — ephemeral linux:
{ "recordUid": "CzYUVX3cW3YOkEu2Jt4bQA", "kind": "start", "conversationType": "ssh", "trickleICE": true, "credentialType": "ephemeral", "jitSettings": {"create_ephemeral": true, "ephemeral_account_type": "linux"} }Scenario 2 — elevation group:
{ "recordUid": "CzYUVX3cW3YOkEu2Jt4bQA", "kind": "start", "conversationType": "ssh", "trickleICE": true, "credentialType": "linked", "userRecordUid": "Q_luxs_ofKp-zQes7A173A", "jitElevation": {"elevate": true, "elevation_method": "group", "elevation_string": "wheel,sudo"} }Scenario 3 — ephemeral + elevation:
{ "recordUid": "CzYUVX3cW3YOkEu2Jt4bQA", "kind": "start", "conversationType": "ssh", "trickleICE": true, "credentialType": "ephemeral", "jitSettings": {"create_ephemeral": true, "ephemeral_account_type": "linux"}, "jitElevation": {"elevate": true, "elevation_method": "group", "elevation_string": "wheel"} }Backward compatibility
--jitis optional; without it,pam launchbehaviour is byte-identical tocurrent
release.jit_settingsbut are launched without-jcontinue to usetheir linked credential / allowSupplyUser / allowSupplyHost path exactly as
before.
PAMLaunchCommand.parser.Notes
release.Made with Cursor