Skip to main content

Trading partner credential vault

Outcome​

A trading partner's SFTP password or private key is rotated through the audited UI, the vault holds the new value, and a connection test confirms the bind.

Prerequisites​

  • The PLATFORM_ADMIN role. The credential write and Test connection both refuse every other platform role.
  • The new credential value provided by the trading partner.
  • Confirm the partner's protocol — AS2 partners can't be probed yet (see pitfalls).

Rotation procedure (UI happy path)​

  1. Open the partner: Configuration → Trading Partners → <partner>.

  2. Open the Credentials tab. Each row shows the current vault reference name, or Not set. The last-rotated timestamp and actor are on the Overview tab. The current credential value is never displayed — there is no view path.

  3. Click Rotate (or Set value for a previously-unset credential).

  4. In the dialog:

    • Choose the credential type (SFTP partners offer SFTP password and SFTP private key).
    • Paste the new value.
    • Type the partner's partner_code (or name if no code is set) into the confirm field. Rotate credential stays disabled until the token matches.
    • Click Rotate credential. The new value is written to the secret trading-partner-<id>-<purpose>; any prior value under that name is overwritten.
  5. Verify with Connection → Run test. The probe runs AzureSftpTransport.healthCheck inside a 10 s timeout.

Test connection responses​

ResponseMeaning
{ healthy: true, latencyMs }SFTP LIST succeeded against remotePath.
{ healthy: false, reason: 'NOT_IMPLEMENTED' }Partner protocol is AS2; not probed yet.
{ healthy: false, reason: 'MISSING_CONFIG' }Host or username not set on connection_config.
{ healthy: false, reason: 'MISSING_CREDENTIAL' }No passwordSecretName / privateKeySecretName, OR the secret is missing in vault.
{ healthy: false, details }SFTP bind failed; details carries the underlying ssh2-sftp-client error. Typically auth failed or connect ETIMEDOUT.

Soft delete and vault purge​

DELETE /:id flips is_active=false and walks credential_secret_names[] calling keyVault.deleteSecret(name) on each entry. Failures are aggregated into the response's failedPurges field — the partner stays soft-deleted regardless. To retry a failed purge, take the secret name from failedPurges (the audit row records only the counts) and purge it with az keyvault secret delete or the Azure portal.

Break-glass: vault-direct CLI​

If the UI is down, a rotation can be performed against Key Vault directly. Write the value under the name the gateway uses, so the name recorded on the partner row keeps pointing at it:

az keyvault secret set \
--vault-name $KEY_VAULT_NAME \
--name "trading-partner-<id>-sftp-private-key" \
--file ./key.pem

If the partner already has that credential recorded, the row needs no change: the build and the probe resolve the recorded name on each use. If it does not, the row must carry the name inside connection_config, which is what the build and the probe read. credential_secret_names alone is only the list the soft delete purges:

UPDATE rcm_master.trading_partner
SET connection_config = jsonb_set(
connection_config, '{credentials,privateKeySecretName}',
to_jsonb('trading-partner-<id>-sftp-private-key'::text), true),
credential_secret_names = array_append(
array_remove(credential_secret_names, 'trading-partner-<id>-sftp-private-key'),
'trading-partner-<id>-sftp-private-key'),
credential_last_rotated_at = NOW(),
credential_last_rotated_by = '<your-user-id>',
updated_at = NOW()
WHERE trading_partner_id = '<id>';

For a password, use sftp-password in the name and passwordSecretName as the key. This break-glass path writes no audit row. Record the change in the change ticket.

Audit trail​

Every partner mutation and connection test writes one row to identity.tenant_audit in the master database, with the partner id in details->>'tradingPartnerId'. The partner-level event types are EDI_TRADING_PARTNER_CREATED, _UPDATED, _DELETED, _CREDENTIAL_WRITTEN and _CONNECTION_TESTED; the Claim.MD lane adds tenant-account, enrollment, appeal and payer-catalogue events. The detail page's Audit tab reads the latest 50 of these, newest first. For older history, query directly:

SELECT occurred_at, platform_user_id, event_type, details, ip_address
FROM identity.tenant_audit
WHERE event_type LIKE 'EDI_TRADING_PARTNER_%'
AND details ->> 'tradingPartnerId' = '<id>'
ORDER BY occurred_at DESC;

The per-tenant audit.access_log is not written: a platform operator has no organization row in a tenant database to satisfy its org_id foreign key.

See Audit log viewer for the global view.

Pitfalls​

  • Don't put secrets in connection_config. The create/PATCH schema is .strict(), so password, privateKey and credentials yield a 400. Always go through the /credentials endpoint. The same constraint is enforced at the repository level, and saving the connection config drops any literal a legacy row still carries.
  • AS2 partners can't be probed yet. test-connection returns NOT_IMPLEMENTED for them. Don't read healthy=false on an AS2 row as an outage signal.
  • Multiple credential types per partner are supported. Setting both sftp-password and sftp-private-key is legal — the probe prefers the private key when both are set, mirroring AzureSftpTransport.buildConnectOptions.

Validation​

CheckExpected
credential_last_rotated_atmatches the rotation moment
credential_secret_names[]includes the new secret
Run testhealthy=true (SFTP partners)
Audit rowone EDI_TRADING_PARTNER_CREDENTIAL_WRITTEN row per rotation in identity.tenant_audit, with platform_user_id

Troubleshooting​

SymptomCauseFix
Rotate button disabled despite typing partner_codeToken mismatch (extra whitespace, case)Re-type exactly.
Probe says auth failed after rotationWrong credential value, or partner expects key auth not passwordConfirm with partner; re-rotate.
Probe says MISSING_CREDENTIALNo secret name recorded in connection_config.credentials, or the vault has no secret by that nameRe-rotate via the UI, which writes both.
failedPurges populated on deleteSecret already gone, or vault permissions issuePurge via Azure portal; the soft-delete already succeeded.

Cross-references​

Next​

2.8 — Authorization workflow + 278 lifecycle UI