[pbs-devel] [PATCH proxmox-backup 2/3] pbs-config: invalidate token-secret cache on token.shadow changes

Samuel Rufinatscha s.rufinatscha at proxmox.com
Fri Dec 5 14:25:55 CET 2025


Previously the in-memory token-secret cache was only updated via
set_secret() and delete_secret(), so manual edits to token.shadow were
not reflected.

This patch adds file change detection to the cache. It tracks the mtime
and length of token.shadow and clears the in-memory token secret cache
whenever these values change.

Note, this patch fetches file stats on every request. An TTL-based
optimization will be covered in a subsequent patch of the series.

This patch is a partly-fix.

[1] https://bugzilla.proxmox.com/show_bug.cgi?id=7017

Signed-off-by: Samuel Rufinatscha <s.rufinatscha at proxmox.com>
---
 pbs-config/src/token_shadow.rs | 35 ++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/pbs-config/src/token_shadow.rs b/pbs-config/src/token_shadow.rs
index 47aa2fc2..ed54cdfa 100644
--- a/pbs-config/src/token_shadow.rs
+++ b/pbs-config/src/token_shadow.rs
@@ -1,5 +1,8 @@
 use std::collections::HashMap;
+use std::fs;
+use std::io::ErrorKind;
 use std::sync::RwLock;
+use std::time::SystemTime;
 
 use anyhow::{bail, format_err, Error};
 use once_cell::sync::OnceCell;
@@ -57,12 +60,38 @@ fn write_file(data: HashMap<Authid, String>) -> Result<(), Error> {
     proxmox_sys::fs::replace_file(CONF_FILE, &json, options, true)
 }
 
+fn refresh_cache_if_file_changed() -> Result<(), Error> {
+    let mut cache = token_secret_cache().write().unwrap();
+
+    // Fetch the current token.shadow metadata
+    let (new_mtime, new_len) = match fs::metadata(CONF_FILE) {
+        Ok(meta) => (meta.modified().ok(), Some(meta.len())),
+        Err(e) if e.kind() == ErrorKind::NotFound => (None, None),
+        Err(e) => return Err(e.into()),
+    };
+
+    // Fast path: file did not change, keep the cache
+    if cache.file_mtime == new_mtime && cache.file_len == new_len {
+        return Ok(());
+    }
+
+    // File changed, drop all cached secrets
+    cache.secrets.clear();
+    cache.file_mtime = new_mtime;
+    cache.file_len = new_len;
+
+    Ok(())
+}
+
 /// Verifies that an entry for given tokenid / API token secret exists
 pub fn verify_secret(tokenid: &Authid, secret: &str) -> Result<(), Error> {
     if !tokenid.is_token() {
         bail!("not an API token ID");
     }
 
+    // Ensure cache is in sync with on-disk token.shadow file
+    refresh_cache_if_file_changed()?;
+
     // Fast path
     if let Some(cached) = token_secret_cache().read().unwrap().secrets.get(tokenid) {
         // Compare cached secret with provided one using constant time comparison
@@ -136,12 +165,18 @@ struct ApiTokenSecretCache {
     /// `generate_and_set_secret`. Used to avoid repeated
     /// password-hash computation on subsequent authentications.
     secrets: HashMap<Authid, String>,
+    // shadow file mtime to detect changes
+    file_mtime: Option<SystemTime>,
+    // shadow file length to detect changes
+    file_len: Option<u64>,
 }
 
 fn token_secret_cache() -> &'static RwLock<ApiTokenSecretCache> {
     TOKEN_SECRET_CACHE.get_or_init(|| {
         RwLock::new(ApiTokenSecretCache {
             secrets: HashMap::new(),
+            file_mtime: None,
+            file_len: None,
         })
     })
 }
-- 
2.47.3





More information about the pbs-devel mailing list