From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id CF1DB1FF0AB for ; Wed, 23 Sep 2026 23:01:09 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B24762170D; Wed, 23 Sep 2026 23:00:15 +0200 (CEST) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox 9/9] access-control: enforce token policy on token create and update Date: Wed, 23 Sep 2026 22:59:58 +0200 Message-ID: <20260923210000.4031318-10-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260923210000.4031318-1-t.lamprecht@proxmox.com> References: <20260923210000.4031318-1-t.lamprecht@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1790197207750 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.683 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: RAVRTKMFFHXDOYSWJZU7UCV55G5WUZ7E X-Message-ID-Hash: RAVRTKMFFHXDOYSWJZU7UCV55G5WUZ7E X-MailFrom: t.lamprecht@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: An update only checks the expiration date if its value actually changes, so unrelated edits or clients resubmitting unchanged fields are not rejected for tokens that predate the policy. The check runs before the secret is generated, or rotated on update, to avoid leaving an orphaned entry in the token shadow file, or losing the old secret, for a rejected request. Signed-off-by: Thomas Lamprecht --- proxmox-access-control/src/api/tokens.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/proxmox-access-control/src/api/tokens.rs b/proxmox-access-control/src/api/tokens.rs index f7417934..58295793 100644 --- a/proxmox-access-control/src/api/tokens.rs +++ b/proxmox-access-control/src/api/tokens.rs @@ -97,6 +97,12 @@ pub fn generate_token( ); } + // check the policy before generating the secret to avoid leaving an orphaned entry in the + // token shadow file on rejection + if let Some(policy) = crate::init::access_conf().token_policy()? { + policy.check_expiry_change(expire.unwrap_or(0), false, proxmox_time::epoch_i64())?; + } + let secret = token_shadow::generate_and_set_secret(&tokenid)?; let token = ApiToken { @@ -180,6 +186,7 @@ pub fn update_token( let tokenid_string = tokenid.to_string(); let mut data: ApiToken = config.lookup("token", &tokenid_string)?; + let old_expire = data.expire.unwrap_or(0); if let Some(delete) = delete { for delete_prop in delete { @@ -206,6 +213,16 @@ pub fn update_token( data.expire = if expire > 0 { Some(expire) } else { None }; } + // enforce the policy only if this update actually changes the expiration date, so unrelated + // edits do not retroactively reject tokens that predate the policy; check before a requested + // secret regeneration so a rejected update does not rotate the secret either + let new_expire = data.expire.unwrap_or(0); + if new_expire != old_expire { + if let Some(policy) = crate::init::access_conf().token_policy()? { + policy.check_expiry_change(new_expire, true, proxmox_time::epoch_i64())?; + } + } + let new_secret = if regenerate == Some(true) { let secret = token_shadow::generate_and_set_secret(&tokenid)?; Some(ApiTokenSecret { tokenid, secret }) -- 2.47.3