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

  • edi.partners.write (typically CONFIG_MANAGER or PLATFORM_ADMIN).
  • The new credential value provided by the trading partner.
  • Confirm partner type — REST and 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 and the last-rotated timestamp. 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 default to sftp-password / sftp-private-key).
    • Paste the new value.
    • Type the partner's partner_code (or name if no code is set) into the confirm field. The Rotate button stays disabled until the token matches.
    • Click Rotate credential. The new value is written to trading-partner/<id>/<purpose>; any prior value under that key 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 REST or 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, fetch the secret name from the audit log and purge directly via the @rcm/key-vault CLI or Azure portal.

Break-glass: vault-direct CLI

If the UI is down, all rotations can be performed against @rcm/key-vault directly:

az keyvault secret set \
--vault-name $KEY_VAULT_NAME \
--name "trading-partner/<id>/sftp-password" \
--value "$NEW_VALUE"

Then update credential_last_rotated_at + credential_secret_names on the row manually:

UPDATE rcm_master.trading_partner
SET credential_last_rotated_at = NOW(),
credential_last_rotated_by = '<your-user-id>',
credential_secret_names = ARRAY['trading-partner/<id>/sftp-password']
WHERE trading_partner_id = '<id>';

This break-glass path bypasses the audit log; record an entry into audit.access_log manually if your retention policy demands it.

Audit trail

Every mutation writes one row into audit.access_log with accessed_entity_type='trading_partner'. The hash chain (migration 019) covers these rows. The Audit tab on the detail page reads the last 50 rows newest-first; for older history query directly:

SELECT accessed_at, actor_user_id, access_action, purpose
FROM audit.access_log
WHERE accessed_entity_type = 'trading_partner'
AND accessed_entity_id = '<id>'
ORDER BY accessed_at DESC;

See Audit log viewer for the global view.

Pitfalls

  • Don't put secrets in connection_config. PATCH's zod schema is .strict() — any unknown key (including credentials) yields a 400. Always go through the /credentials endpoint. The same constraint is enforced at the repository level.
  • REST partners can't be probed yet. test-connection returns NOT_IMPLEMENTED until a real REST partner ships. Don't read healthy=false on a REST 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 entry per rotation with actor_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_CREDENTIALVault write failed, or credential_secret_names not updatedInspect the audit row; re-rotate via UI to fix the row.
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