|
| 1 | +--- |
| 2 | +name: a2a-workflow |
| 3 | +description: Use when working with Safeguard A2A certificate auth, credential retrieval, brokering, or A2A event listeners. |
| 4 | +--- |
| 5 | + |
| 6 | +# A2A Workflow |
| 7 | +Safeguard A2A (Application-to-Application) is the SDK surface for unattended, |
| 8 | +certificate-based integrations. In this repo, `Safeguard.A2A.GetContext(...)` |
| 9 | +creates an `ISafeguardA2AContext` that talks to `/service/A2A/v<apiVersion>/...` |
| 10 | +with a client certificate plus an A2A API key. It is used for retrieving passwords, |
| 11 | +SSH keys, API key secrets, brokering access requests, and subscribing to A2A |
| 12 | +credential-change events. |
| 13 | +## 1. What A2A is (one-paragraph context) |
| 14 | + |
| 15 | +A2A is the automation-friendly side of Safeguard. Unlike normal `Safeguard.Connect()` |
| 16 | +flows that authenticate a user and then send bearer tokens, A2A uses a client |
| 17 | +certificate to establish trust and an `Authorization: A2A <apiKey>` header to scope |
| 18 | +credential retrieval or brokering to a configured registration. The SDK models this |
| 19 | +with `ISafeguardA2AContext`, `SafeguardA2AContext`, `A2ARetrievableAccount`, |
| 20 | +`BrokeredAccessRequest`, and `ApiKeySecret`. |
| 21 | +## 2. Setup flow (certificate registration, API key creation) |
| 22 | + |
| 23 | +The repository does not create A2A registrations for you; that setup happens on the |
| 24 | +Safeguard appliance. The codebase shows the expected appliance-side shape. |
| 25 | + |
| 26 | +### Appliance-side prerequisites |
| 27 | + |
| 28 | +1. Register a client certificate for the integration. |
| 29 | +2. Create one or more A2A registrations tied to that certificate user. |
| 30 | +3. Assign retrievable accounts and/or brokering rights. |
| 31 | +4. Generate the A2A API key that will be used for retrieval or brokering. |
| 32 | + |
| 33 | +### SDK context creation patterns |
| 34 | + |
| 35 | +`Safeguard.A2A.GetContext(...)` supports the same three certificate sources used by |
| 36 | +other certificate-based SDK entry points: |
| 37 | + |
| 38 | +- certificate store thumbprint |
| 39 | +- PFX/PKCS#12 file + password |
| 40 | +- in-memory certificate bytes + password |
| 41 | + |
| 42 | +Examples: |
| 43 | + |
| 44 | +```csharp |
| 45 | +using var context = Safeguard.A2A.GetContext( |
| 46 | + appliance, |
| 47 | + thumbprint, |
| 48 | + apiVersion: 4, |
| 49 | + ignoreSsl: true); |
| 50 | +``` |
| 51 | + |
| 52 | +```csharp |
| 53 | +using var context = Safeguard.A2A.GetContext( |
| 54 | + appliance, |
| 55 | + certificatePath, |
| 56 | + certificatePassword, |
| 57 | + apiVersion: 4, |
| 58 | + ignoreSsl: true); |
| 59 | +``` |
| 60 | + |
| 61 | +```csharp |
| 62 | +var bytes = File.ReadAllBytes(certificatePath); |
| 63 | +using var context = Safeguard.A2A.GetContext( |
| 64 | + appliance, |
| 65 | + bytes, |
| 66 | + certificatePassword, |
| 67 | + apiVersion: 4, |
| 68 | + ignoreSsl: true); |
| 69 | +``` |
| 70 | + |
| 71 | +Validation-callback overloads also exist when you want custom certificate validation |
| 72 | +instead of `ignoreSsl`. |
| 73 | + |
| 74 | +### Enumerating registrations and API keys |
| 75 | + |
| 76 | +`Samples\SampleA2aService\SampleService.cs` shows a practical discovery flow: |
| 77 | + |
| 78 | +1. Create a normal certificate-backed `ISafeguardConnection` |
| 79 | +2. Query `Service.Core` `A2ARegistrations` |
| 80 | +3. Filter by `CertificateUserThumbprint` |
| 81 | +4. Query `A2ARegistrations/{id}/RetrievableAccounts` |
| 82 | +5. Read the returned `ApiKey` values and cache them as `SecureString` |
| 83 | + |
| 84 | +That sample uses: |
| 85 | + |
| 86 | +```csharp |
| 87 | +var a2AJson = _connection.InvokeMethod( |
| 88 | + Service.Core, |
| 89 | + Method.Get, |
| 90 | + "A2ARegistrations", |
| 91 | + parameters: new Dictionary<string, string> |
| 92 | + { |
| 93 | + ["filter"] = $"CertificateUserThumbprint ieq '{thumbprint}'", |
| 94 | + }); |
| 95 | +``` |
| 96 | + |
| 97 | +Important setup notes pulled from the repo: |
| 98 | + |
| 99 | +- `ISafeguardA2AContext.GetRetrievableAccounts()` is documented as a Safeguard v2.8+ |
| 100 | + feature that must be enabled in the A2A configuration. |
| 101 | +- The sample comments note that enumerating registrations by certificate user may |
| 102 | + require auditor permission. |
| 103 | +- `SampleA2aService` throws if no API keys are found after enumeration. |
| 104 | + |
| 105 | +## 3. Credential retrieval (programmatic access) |
| 106 | + |
| 107 | +### Primary retrieval methods on `ISafeguardA2AContext` |
| 108 | + |
| 109 | +| Method | Purpose | |
| 110 | +|---|---| |
| 111 | +| `GetRetrievableAccounts()` / `GetRetrievableAccounts(filter)` | Discover accessible accounts and API keys | |
| 112 | +| `RetrievePassword(apiKey)` | Fetch a password as `SecureString` | |
| 113 | +| `SetPassword(apiKey, password)` | Rotate/update a password | |
| 114 | +| `RetrievePrivateKey(apiKey, keyFormat)` | Fetch an SSH private key | |
| 115 | +| `SetPrivateKey(apiKey, privateKey, password, keyFormat)` | Upload/update an SSH private key | |
| 116 | +| `RetrieveApiKeySecret(apiKey)` | Fetch API key secret material as `IList<ApiKeySecret>` | |
| 117 | + |
| 118 | +### Transport details |
| 119 | + |
| 120 | +`SafeguardA2AContext` uses these routes internally: |
| 121 | + |
| 122 | +- `GET Core/A2ARegistrations` |
| 123 | +- `GET Core/A2ARegistrations/{id}/RetrievableAccounts` |
| 124 | +- `GET A2A/Credentials?type=Password` |
| 125 | +- `PUT A2A/Credentials/Password` |
| 126 | +- `GET A2A/Credentials?type=PrivateKey&keyFormat=<format>` |
| 127 | +- `PUT A2A/Credentials/SshKey?keyFormat=<format>` |
| 128 | +- `GET A2A/Credentials?type=ApiKey` |
| 129 | + |
| 130 | +The context sends: |
| 131 | + |
| 132 | +- `Accept: application/json` |
| 133 | +- `Authorization: A2A <apiKey>` when an API key is required |
| 134 | +- the client certificate on the TLS connection |
| 135 | + |
| 136 | +### Typical password retrieval pattern |
| 137 | + |
| 138 | +```csharp |
| 139 | +using var context = Safeguard.A2A.GetContext(appliance, thumbprint, apiVersion: 4, ignoreSsl: true); |
| 140 | +using var password = context.RetrievePassword(apiKey.ToSecureString()); |
| 141 | + |
| 142 | +var clearText = password.ToInsecureString(); |
| 143 | +``` |
| 144 | + |
| 145 | +### Discovering retrievable accounts |
| 146 | + |
| 147 | +`Test\SafeguardDotNetA2aTool` uses `GetRetrievableAccounts()` in two modes: |
| 148 | + |
| 149 | +- no filter -> enumerate everything visible to the registration |
| 150 | +- SCIM-style filter -> e.g. `AccountName eq 'admin'` |
| 151 | + |
| 152 | +The filter is applied server-side to each registration's retrievable-accounts endpoint. |
| 153 | + |
| 154 | +### API key secret retrieval |
| 155 | + |
| 156 | +`RetrieveApiKeySecret()` returns `ApiKeySecret` objects whose `ClientSecret` is a |
| 157 | +`SecureString`. Dispose them when you are done. |
| 158 | + |
| 159 | +### Secure disposal expectations |
| 160 | + |
| 161 | +- `ApiKeySecret` implements `IDisposable` |
| 162 | +- `A2ARetrievableAccount.ApiKey` is treated as sensitive data |
| 163 | +- certificate passwords and retrieved credentials should stay in `SecureString` |
| 164 | +- top-level services such as `SampleA2aService` dispose listeners, connections, and |
| 165 | + A2A contexts during shutdown |
| 166 | + |
| 167 | +## 4. Brokering (if supported) |
| 168 | + |
| 169 | +Yes. `ISafeguardA2AContext.BrokerAccessRequest()` posts a `BrokeredAccessRequest` |
| 170 | +object to `A2A/AccessRequests`. |
| 171 | + |
| 172 | +### Minimum required fields |
| 173 | + |
| 174 | +`SafeguardA2AContext` enforces these before the HTTP call: |
| 175 | + |
| 176 | +- one of `ForUserId` or `ForUserName` |
| 177 | +- one of `AssetId` or `AssetName` |
| 178 | + |
| 179 | +If either is missing, it throws `SafeguardDotNetException` before contacting the |
| 180 | +server. |
| 181 | + |
| 182 | +### Useful optional fields from `BrokeredAccessRequest` |
| 183 | + |
| 184 | +- `AccessType` (`Password`, `Ssh`, `Rdp`) |
| 185 | +- `AccountId` / `AccountName` |
| 186 | +- `AccountAssetId` / `AccountAssetName` |
| 187 | +- `ReasonCodeId` / `ReasonCode` |
| 188 | +- `ReasonComment` |
| 189 | +- `TicketNumber` |
| 190 | +- `RequestedFor` |
| 191 | +- `RequestedDuration` |
| 192 | + |
| 193 | +`Test\SafeguardDotNetAccessRequestBrokerTool` shows the intended calling pattern: |
| 194 | + |
| 195 | +```csharp |
| 196 | +using var context = CreateA2AContext(opts); |
| 197 | +var accessRequest = GetBrokeredAccessRequestObject(opts); |
| 198 | +var json = context.BrokerAccessRequest(opts.ApiKey.ToSecureString(), accessRequest); |
| 199 | +``` |
| 200 | + |
| 201 | +The tool accepts either IDs or names for user, asset, account, and reason code, then |
| 202 | +maps numeric strings to the `*Id` properties. |
| 203 | + |
| 204 | +## 5. Event listeners / SignalR (if supported) |
| 205 | + |
| 206 | +Yes. A2A supports both non-persistent and persistent SignalR listeners. |
| 207 | + |
| 208 | +### Context-based listener APIs |
| 209 | + |
| 210 | +| Method | Recovery behavior | |
| 211 | +|---|---| |
| 212 | +| `GetA2AEventListener(apiKey, handler)` | Does **not** recover from a 30+ second outage | |
| 213 | +| `GetA2AEventListener(apiKeys, handler)` | Same, but for multiple API keys | |
| 214 | +| `GetPersistentA2AEventListener(apiKey, handler)` | Reconnects automatically | |
| 215 | +| `GetPersistentA2AEventListener(apiKeys, handler)` | Reconnects automatically | |
| 216 | + |
| 217 | +### Static helper APIs |
| 218 | + |
| 219 | +`Safeguard.A2A.Event.GetPersistentA2AEventListener(...)` exposes the same persistent |
| 220 | +listener pattern directly from: |
| 221 | + |
| 222 | +- thumbprint-based certificate auth |
| 223 | +- certificate file + password |
| 224 | +- in-memory certificate bytes + password |
| 225 | +- optional validation-callback overloads |
| 226 | +- single API key or multiple API keys |
| 227 | + |
| 228 | +### Event names actually registered |
| 229 | + |
| 230 | +The implementation registers handlers for three A2A events: |
| 231 | + |
| 232 | +- `AssetAccountPasswordUpdated` |
| 233 | +- `AssetAccountSshKeyUpdated` |
| 234 | +- `AccountApiKeySecretUpdated` |
| 235 | + |
| 236 | +This is worth knowing because some XML comments still describe only the password |
| 237 | +update event. |
| 238 | + |
| 239 | +### Reconnect behavior |
| 240 | + |
| 241 | +Persistent A2A listeners are backed by `PersistentSafeguardA2AEventListener`, which |
| 242 | +inherits the shared reconnect loop from `PersistentSafeguardEventListenerBase`: |
| 243 | + |
| 244 | +- reconnect work runs in the background |
| 245 | +- failures log a warning and sleep for 5 seconds |
| 246 | +- the listener retries until reconnection succeeds or stop/dispose is requested |
| 247 | + |
| 248 | +Use the persistent variant for Windows services, daemon-style workloads, or anything |
| 249 | +that must survive appliance/network interruptions. |
| 250 | + |
| 251 | +### Sample patterns in this repo |
| 252 | + |
| 253 | +- `Samples\SampleA2aService\SampleService.cs` starts one persistent listener per API key |
| 254 | +- `Test\SafeguardDotNetEventTool` supports single-key, multi-key, and "discover all keys" |
| 255 | + flows before starting a listener |
| 256 | +- both patterns call `Start()` explicitly after creating the listener |
| 257 | + |
| 258 | +## 6. Error scenarios and troubleshooting |
| 259 | + |
| 260 | +### Common argument/setup failures |
| 261 | + |
| 262 | +- No certificate input -> the CLI tools throw `InvalidOperationException("Must specify CertificateFile or Thumbprint")` |
| 263 | +- Null `apiKey`, `password`, `privateKey`, or `accessRequest` -> `ArgumentException` |
| 264 | +- Empty API key set for multi-key listeners -> `ArgumentException` |
| 265 | +- Missing user or asset in brokered requests -> `SafeguardDotNetException` |
| 266 | + |
| 267 | +### HTTP/API failures |
| 268 | + |
| 269 | +`SafeguardA2AContext.ApiRequest()` throws `SafeguardDotNetException` when Safeguard |
| 270 | +returns a non-success HTTP status. The exception includes status and raw response |
| 271 | +content, so inspect `HttpStatusCode`, `ErrorMessage`, and `Response`. |
| 272 | + |
| 273 | +### Timeouts and retries |
| 274 | + |
| 275 | +- one-shot retrieval/brokering calls do **not** include an automatic retry loop |
| 276 | +- timeout surfaces as `SafeguardDotNetException("Request timeout to ...")` |
| 277 | +- retry policy for `RetrievePassword()`, `RetrievePrivateKey()`, or `BrokerAccessRequest()` |
| 278 | + is the caller's responsibility |
| 279 | +- only persistent SignalR listeners auto-reconnect |
| 280 | + |
| 281 | +### SSL troubleshooting |
| 282 | + |
| 283 | +- `ignoreSsl` bypasses certificate validation and is intended for dev/test only |
| 284 | +- production integrations should prefer trusted certificates or a validation callback |
| 285 | +- the same certificate source pattern is reused across normal SDK connections and A2A |
| 286 | + |
| 287 | +### Registration discovery troubleshooting |
| 288 | + |
| 289 | +If registration enumeration fails in the sample flow, check: |
| 290 | + |
| 291 | +- whether the certificate thumbprint matches the A2A registration's certificate user |
| 292 | +- whether the registration has retrievable accounts configured |
| 293 | +- whether the appliance version/config supports `GetRetrievableAccounts()` |
| 294 | +- whether the caller has permission to enumerate `A2ARegistrations` |
| 295 | + |
| 296 | +If you only need retrieval and already have a valid API key, skip the Core registration |
| 297 | +lookup and call the A2A context methods directly. |
0 commit comments