Two Key Vault SDKs, One Password in Plain Sight
One class bridging the legacy ADAL KeyVaultClient and the modern SecretClient, three coexisting secret postures - managed identity, a service-principal password in config, a committed key file - and a migration frozen mid-flight.
A key vault is only as good as the credential that opens it, and the attendance platform opened its vault three different ways — one exemplary, one compromised, one that undoes the entire point of having a vault. All three lived behind a single class that straddled two generations of Azure SDK, a migration someone started and never finished. This part is about that class, the three secret postures it served, and the uncomfortable truth that a vault with its password written next to it is just an encrypted file with extra steps. It builds directly on the email transport from part twelve, whose Gmail path is one of the postures below.
One class, two SDKs
Azure shipped two entirely separate client libraries for Key Vault. The old world was Microsoft.Azure.KeyVault — a KeyVaultClient authenticated through ADAL, the Active Directory Authentication Library, with an AuthenticationContext you fed credentials to acquire a token. The new world is Azure.Security.KeyVault.Secrets — a SecretClient authenticated through DefaultAzureCredential, which tries managed identity, environment, and developer credentials in turn without you handling a token at all.
The platform's vault wrapper had both, chosen at runtime by a configuration flag:
public string GetSecret(string name)
{
if (_accessType == "Web") // modern path
{
var client = new SecretClient(new Uri(_vaultUri), new DefaultAzureCredential());
return client.GetSecret(name).Value.Value;
}
else // legacy path: "Service" / "Job"
{
var auth = new AuthenticationContext(_authority);
var token = auth.AcquireTokenAsync(
"https://vault.azure.net",
new ClientCredential(_appId, _appPassword)).Result; // blocking, and note _appPassword
var client = new KeyVaultClient(...);
return client.GetSecretAsync(_vaultUri, name).Result.Value;
}
}
One class, two authentication universes, a runtime branch between them. This is the field mark of an abandoned migration from part seven, now in the security layer: someone began moving from ADAL to DefaultAzureCredential, converted the web application's path, and stopped before the batch executables followed. The web app uses the clean modern path; the jobs still use the legacy one — and the legacy one, note, blocks on .Result (ADAL's async over a sync boundary, the sync-over-async hazard again) and, more importantly, needs a ClientCredential(appId, appPassword). Which is where the trouble is.
Three postures, ranked
The estate opened its vault three ways, and they form a clean ladder from best to worst.
Posture one — managed identity (the web app). DefaultAzureCredential on an App Service with a managed identity assigned means the platform authenticates to Key Vault with no secret at all. Azure issues the App Service an identity; the vault's access policy grants that identity read on secrets; nothing anywhere holds a password. This is the target state, and the web app reached it. There is no credential to leak because there is no credential. Full marks — and worth stating plainly, because the rest of this post is critical and this part was genuinely done right.
Posture two — service-principal password in config (a batch job). The console job could not, in its deployment, use managed identity, so it authenticated as a service principal: an app id and a password. That password — along with the tenant id and the app id — sat in a plaintext appsettings.json committed to the repository. Now walk the logic: the connection string is in Key Vault, safely. To read Key Vault, the job needs the service-principal password. The service-principal password is in a file next to the code. The vault protecting the connection string is unlocked by a password sitting in plain sight beside it — so the connection string is, in effect, as exposed as the least-protected credential in the chain, and that credential is in source control. The vault added ceremony, not protection. An attacker with the repo has the SP password, and the SP password has the vault.
Posture three — a committed key file (the Gmail sender). The worst. The email sender that used the Gmail API (from part twelve) authenticated as a Google service account using a P12 private key file committed directly to the repository, with the file's passphrase hard-coded in the source. The passphrase, I will note precisely because the map permits it, was notasecret — Google's documented default passphrase for these exported key files, not a chosen password. That default exists because the P12's security is supposed to come from never letting the file leave a trusted location; the passphrase is a formality. Committing the file to source control removes the one protection the design assumed. A service-account private key in a git history is a standing credential that survives every branch, clone, and fork, cannot be rotated by editing a config value, and grants — via domain-wide delegation — the ability to send mail as the organisation. The lesson is the pattern, not the file: a private key committed to a repository is compromised the moment it is committed, whether or not anyone has noticed yet, and the only remediation is rotation, never deletion. Removing the file from the tip does nothing; git remembers.
The vault theatre
Step back and the three postures tell one story. The platform had a Key Vault. It went to the trouble of provisioning one, wiring an SDK to it, storing connection strings in it. And two of its three executables reached that vault with credentials less protected than the secrets inside it. That is the anti-pattern I have come to call vault theatre: the appearance of secret management — there is a vault, there is a client, there is a secret name — wrapped around a bootstrap credential that lives in the clear. The secret is only as safe as the credential that fetches it, and a fetch-credential in appsettings.json or a committed key file caps your whole security at the level of “in the repo”.
The uncomfortable question every vault deployment should have to answer: how does this process authenticate to the vault, and where does that credential live? If the answer is “a password in config” or “a key file in the repo”, the vault is decoration. If the answer is “a managed identity, so there is no credential”, the vault is doing its job. The web app could answer well; the jobs could not, and the gap between them is the whole security posture of the estate in one comparison. The companion series revisits this from the console job's side in its reckoning.
What finishing the migration looks like
The remediation is not exotic, and most of it is deployment work, not code:
- Give every executable a managed identity. Azure Functions and even externally-scheduled compute can carry one; the console job's SP-password path exists because nobody assigned it an identity, not because it couldn't have one.
- Delete the legacy ADAL path once the jobs use
DefaultAzureCredential— one branch, not two, and no.Resultblocking. - Rotate the committed Google key immediately, issue a new one, and store it in the vault the platform already had — a service-account key can live in Key Vault and be fetched by managed identity, closing the loop the platform left open.
- Purge the secrets from git history, treating every value that was ever committed as burned. The connection string that sat in config, the SP password, the P12 — all of them need rotating, because “we removed it” is not remediation for a value a clone already holds.
The ledger
- The modern path — managed identity, no credential, exemplary. This is what the whole estate should have looked like.
- The frozen migration — two SDKs in one class is the security-layer version of an abandoned crossing; finish it and delete the old half.
- SP password in config — vault theatre; the fetch-credential was less protected than the secret it fetched.
- The committed key file — the worst posture; compromised on commit, remediable only by rotation. The pattern is publishable; the value is radioactive and appears nowhere.
The Key Vault SDK is the last of a long parade of packages in this app — some used well, several referenced and never called, one an EF6 library in an EF Core project. Time to tour the whole cemetery: The Dependency Graveyard.