-
Notifications
You must be signed in to change notification settings - Fork 380
[MSI v2] Cache MSI v2 cert #5557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MSI v2] Cache MSI v2 cert #5557
Conversation
| internal static readonly ICertificateCache s_mtlsCertificateCache = new InMemoryCertificateCache(); | ||
|
|
||
| // Per-key async de-duplication so concurrent callers don’t double-mint. | ||
| internal static readonly ConcurrentDictionary<string, SemaphoreSlim> s_perKeyGates = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This design introduces unnecessary coupling and breaks encapsulation, leading to class envy. The responsibility should either reside where it’s used or be abstracted into a dedicated component.
Additionally, usage should avoid direct field access. Instead, expose functionality through well-defined internal or public methods behind a thoughtful interface.
For instance, why isn’t s_perKeyGates treated as an implementation detail of the imdsv2 component, given that it appears to be used only there? The same applies to the cache instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
udpated
| Endpoint = endpoint ?? throw new MsalClientException(nameof(endpoint)); | ||
| ClientId = clientId ?? throw new MsalClientException(nameof(clientId)); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldnt we need comments for public members?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are internal
| /// </summary> | ||
| bool TryGet( | ||
| string cacheKey, | ||
| out X509Certificate2 certificate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very classic way of doing it, wouldnt it be better to use a tuple, valuetype result or even a record/structure here so that we have some flexibility with adding fields without breaking the contract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
| /// Process-local cache for an mTLS certificate and its endpoint. | ||
| /// Expiration is based solely on certificate.NotAfter. | ||
| /// </summary> | ||
| internal interface ICertificateCache : IDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say that it shouldnt inerit form IDisposable, but the implementation has.
| /// certificate+endpoint+clientId cache stored in process memory. | ||
| /// </summary> | ||
| internal sealed class InMemoryCertificateCache : ICertificateCache | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should implement IDisposable, not the interface, imho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, updated
| } | ||
|
|
||
| /// <summary> | ||
| /// used for logging cache keys without exposing full values. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Start from capital letter
| /// <summary> | ||
| /// used for logging cache keys without exposing full values. | ||
| /// </summary> | ||
| /// <param name="s"></param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we fill those up as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated but this is internal
| { | ||
| if (string.IsNullOrEmpty(s)) | ||
| return "<empty>"; | ||
| var take = Math.Min(8, s.Length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so it would expose full value if it is less than 8? Is that ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
| using (var httpManager = new MockHttpManager()) | ||
| { | ||
| // Start clean across tests | ||
| ManagedIdentityClient.ResetSourceForTest(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you ensure that those tests dont race? Because we are using static state here, this could cause instabilities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed this - and each test before initializing will always call ResetSourceForTest, so the static caches are cleared in PRODUCT.
|
|
||
| internal static void ResetSourceForTest() | ||
| { | ||
| s_sourceName = ManagedIdentitySource.None; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we leak the dependency to test, this utility metod can belong in test itself....
A better approach would be to expose appropriate public methods on well encapsulated components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved the one in IMDS V2 to it's own method and call it from here
…_cert - Resolve conflicts in ManagedIdentityClient.cs and ImdsV2ManagedIdentitySource.cs - Update tests (ImdsV2Tests, MockHelpers)
| return; // already disposed | ||
| } | ||
|
|
||
| try |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try/catch is expensive. You won't double dispose with that atomic switch you got on line 70, would you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
| /// </summary> | ||
| void Set( | ||
| string cacheKey, | ||
| X509Certificate2 certificate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we adjust the set as well? This looks better actually ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree - missed updating this
commit 1 - caches MSI v2 in-memory + tests (ready)
commit 2 - stores cert to store and if there is a cache miss, checks store (in-progress)
commit 3 - adds mutex (in-progress)
This PR adds a minimal, internal in‑memory certificate cache for IMDS v2 mTLS PoP:
Why 24 hours?
mTLS PoP tokens can be valid up to 24h; reusing a cert that expires sooner would create a token that cannot be used by the resource throughout its validity. The cache therefore refuses to store certs with < 24h remaining and evicts entries that have hit their NotAfter.