From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v8 4/6] token shadow: add TTL window to token secret cache
Date: Thu, 9 Apr 2026 17:54:24 +0200 [thread overview]
Message-ID: <20260409155437.312760-5-s.rufinatscha@proxmox.com> (raw)
In-Reply-To: <20260409155437.312760-1-s.rufinatscha@proxmox.com>
cached_secret_valid() currently stats the file on every request, which
performs a metadata() call on token.shadow each time. Under load this
adds unnecessary overhead, considering also the file usually should
rarely change.
This patch introduces a TTL boundary, controlled by
TOKEN_SECRET_CACHE_TTL_SECS. File metadata is only re-loaded once the
TTL has expired.
Signed-off-by: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
---
Changes from v7 to v8:
* TTL fast path (read lock) and write-lock re-check now return
cache.secret_matches(tokenid, secret) instead of just true, following
the cached_secret_valid() merge from the previous patch
* Adjusted commit message
Changes from v6 to v7:
* Rebased
Changes from v5 to v6:
* Rebased
Changes from v4 to v5:
* Rebased
* Introduce shadow_check_within_ttl() helper
Changes from v3 to v4:
* Adjusted commit message
Changes from v2 to v3:
* Refactored refresh_cache_if_file_changed TTL logic.
* Remove had_prior_state check (replaced by last_checked logic).
* Improve TTL bound checks.
* Reword documentation warning for clarity.
Changes from v1 to v2:
* Add TOKEN_SECRET_CACHE_TTL_SECS and last_checked.
* Implement double-checked TTL: check with try_read first; only attempt
refresh with try_write if expired/unknown.
* Fix TTL bookkeeping: update last_checked on the “file unchanged” path
and after API mutations.
* Add documentation warning about TTL-delayed effect of manual
token.shadow edits.
proxmox-access-control/src/token_shadow.rs | 33 +++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/proxmox-access-control/src/token_shadow.rs b/proxmox-access-control/src/token_shadow.rs
index 810ff0c3..4185351e 100644
--- a/proxmox-access-control/src/token_shadow.rs
+++ b/proxmox-access-control/src/token_shadow.rs
@@ -27,6 +27,8 @@ static TOKEN_SECRET_CACHE: LazyLock<RwLock<ApiTokenSecretCache>> = LazyLock::new
file_info: None,
})
});
+/// Max age in seconds of the token secret cache before checking for file changes.
+const TOKEN_SECRET_CACHE_TTL_SECS: i64 = 60;
// Get exclusive lock
fn lock_config() -> Result<ApiLockGuard, Error> {
@@ -57,15 +59,31 @@ fn write_file(data: HashMap<Authid, String>) -> Result<(), Error> {
/// tokens for in-flight requests may still validate against the previous
/// generation.
///
+/// If the cache file metadata's TTL has expired, will revalidate and invalidate the cache if
+/// needed.
+///
/// Returns true if secret is cached and cache is still valid
fn cached_secret_valid(tokenid: &Authid, secret: &str) -> bool {
let now = epoch_i64();
- // Best-effort refresh under write lock.
+ // Fast path: cache is fresh if generation matches and TTL not expired.
+ if let (Some(cache), Some(read_gen)) =
+ (TOKEN_SECRET_CACHE.try_read(), token_shadow_generation())
+ {
+ if cache.cached_gen == read_gen && cache.shadow_check_within_ttl(now) {
+ return cache.secret_matches(tokenid, secret);
+ }
+ // read lock drops here
+ } else {
+ return false;
+ }
+
+ // Slow path: best-effort refresh under write lock.
let Some(mut cache) = TOKEN_SECRET_CACHE.try_write() else {
return false;
};
+ // Re-read generation after acquiring the lock (may have changed meanwhile).
let Some(current_gen) = token_shadow_generation() else {
return false;
};
@@ -75,6 +93,12 @@ fn cached_secret_valid(tokenid: &Authid, secret: &str) -> bool {
cache.reset_and_set_gen(current_gen);
}
+ // TTL check again after acquiring the lock
+ let now = epoch_i64();
+ if cache.shadow_check_within_ttl(now) {
+ return cache.secret_matches(tokenid, secret);
+ }
+
// Stat the file to detect manual edits.
let Ok((new_mtime, new_len)) = shadow_mtime_len() else {
return false;
@@ -224,6 +248,13 @@ impl ApiTokenSecretCache {
self.cached_gen = new_gen;
}
+ /// Returns true if cached token.shadow metadata exists and was checked within the TTL window.
+ fn shadow_check_within_ttl(&self, now: i64) -> bool {
+ self.file_info.as_ref().is_some_and(|cached| {
+ now >= cached.last_checked && (now - cached.last_checked) < TOKEN_SECRET_CACHE_TTL_SECS
+ })
+ }
+
/// Returns true if there is a matching cached entry
fn secret_matches(&self, tokenid: &Authid, secret: &str) -> bool {
let Some(entry) = self.secrets.get(tokenid) else {
--
2.47.3
next prev parent reply other threads:[~2026-04-09 15:54 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-09 15:54 [PATCH proxmox{,-datacenter-manager} v8 0/9] token-shadow: reduce api token verification overhead Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 1/6] token shadow: split AccessControlConfig and add token.shadow generation Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 2/6] token shadow: cache verified API token secrets Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 3/6] token shadow: invalidate token-secret cache on token.shadow changes Samuel Rufinatscha
2026-04-09 15:54 ` Samuel Rufinatscha [this message]
2026-04-09 15:54 ` [PATCH proxmox v8 5/6] token shadow: inline set_secret fn Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 6/6] token shadow: deduplicate more code into apply_api_mutation Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 1/3] pdm-config: implement access control backend hooks Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 2/3] pdm-config: wire user and ACL cache generation Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 3/3] pdm-config: wire token.shadow generation Samuel Rufinatscha
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260409155437.312760-5-s.rufinatscha@proxmox.com \
--to=s.rufinatscha@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.