Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions integration-tests/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ impl TestHarness {
public_key_path: auth_pubkey_path.as_path().to_path_buf(),
}],
}),
roles: Vec::new(),
},
AdminType::DenyAll => AdminConfig {
admin_backend: AdminBackendType::DenyAll,
roles: Vec::new(),
},
};

Expand Down
33 changes: 29 additions & 4 deletions kbs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,23 @@ Detailed [documentation](https://docs.trustauthority.intel.com).

### Admin API Configuration

Multiple Admin backends are available. These control access to admin endpoints such as `set_policy`.
Today, the available backends are `DenyAll` (disables admin endpoints), `InsecureAllowAll` (for debugging)
and `Simple`.
The admin configuration has two parts.
The admin backend is used for validating the admin token and extracting a role.
The admin configuration can also specify which endpoints the role has access to.


Multiple Admin backends are available.
Today, the available backends are `DenyAll` (rejects all admin tokens, thereby blocking all admin interfaces),
`InsecureAllowAll` (treats every request as if there is a valid admin token and return the role `Anonymous`)
and `Simple` (checks the admin token based on a set of public keys).

By default, the simple backend will be used, but no personas will be enabled.
Use the `type` field to set the admin backend.

| Property | Type | Description | Required | Default |
|-------------------|---------|-------------------------------------------------------------------|----------|---------|
| `type` | String | The backend used to validate admiin requests. | No | Simple |
| `type` | String | The backend used to validate admin requests. | No | Simple |
| `roles` | | Rules for accessing admin endpoints | No | Empty |

If the `Simple` backend is used, a list of admin personas can be provided, each with the following properties:

Expand All @@ -203,6 +211,23 @@ If the `Simple` backend is used, a list of admin personas can be provided, each
| `id` | String | A string used to identify the admin. | Yes | Simple |
| `public_key_path` | String | The path to the public key corresponding to the admin token. | Yes | Simple |

The role field can specify rules that govern which endpoints an admin is allowed to visit.
If no roles are specified, all admin can access all admin endpoints.
Otherwise, the roles field should be a list of role entries.
One role entry looks like this.

| Property | Type | Description | Required |
|-------------------|--------|----------------------------------------------------------|----------|
| id | String | The role that this rule applies to | Yes |
| allowed_endpoints | String | A regular expression matching endpoints that are allowed | Yes |

After the admin token is validated, the role extracted from the token will be checked against
the roles specified in the config file.
If a matching role is found, the `allowed_endpoints` regular expression is used to determine
which endpoints the admin can access.
If no matching role is found (and at least one role is specified), the admin will not be able
to access any endpoints.
Roles and role ids are case insensitive.

### Policy Engine Configuration

Expand Down
4 changes: 2 additions & 2 deletions kbs/src/admin/allow_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::admin::AdminBackend;
pub struct InsecureAllowAllBackend {}

impl AdminBackend for InsecureAllowAllBackend {
fn validate_admin_token(&self, _request: &HttpRequest) -> Result<()> {
fn validate_admin_token(&self, _request: &HttpRequest) -> Result<String> {
warn!("Allow All admin backend is set. Anyone can access admin APIs");
Ok(())
Ok("Anonymous".to_string())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

  1. have a separate policy file (no need to be rego, but if rego is good we can use rego) to define admin authZ things (What this PR is intending to do)
  2. the input to the the id/subject or claims parsed from the input JWT of header's bearer.
  3. The output is true/false.

We can by default give a allow all/deny all policy.

In this way we can have a simple authZ system that is code-policy-decoupled.

The policy can includes all the "role" names and its acceptable paths (e.g. in regex)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's too complicated. Users are already confused by the two policies we have. Let's only add another one if we absolutely have to. What is implemented here is basically a policy mechanism, but it's made as simple as possible. Let's not do anything more complex unless users explicitly ask for it.

Copy link
Member

@Xynnn007 Xynnn007 Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, what we are aiming for right now in the admin module is a simple authorization (AuthZ) mechanism, i.e., defining which APIs can be accessed.

The solution proposed in this PR enables a quick way to define roles and their accessible APIs, and it is ready for deployment. This approach is very useful for some internal or short-term projects where simplicity and fast iteration are the primary goals.

However, from a longer-term evolution perspective—especially if we expect more frequent policy changes or additional authorization scenarios - there are a few concerns worth discussing:

  1. Authorization rules are embedded in startup configuration

Currently, the rules are defined in the KBS configuration at startup time and are not independently versioned or reloadable. As a result, updating or modifying authorization rules requires restarting the entire KBS, which may become inconvenient as policies evolve.

2.Mixed abstraction between token verification and authorization decision

The current validate_admin_token abstraction mixes two different concerns:

  • Token Verification / Identity Resolution (e.g., extracting credentials, verifying signatures, validating issuers), and
  • Authorization Decision (allow / deny, or mapping to accessible APIs).

In retrospect, this separation was not clearly enforced in earlier designs (including my own previous review in https://github.com/confidential-containers/trustee/pull/1014/changes#diff-57ec221f4d45eb2d52007f10e72821894545bdea23a2ca94924f4a55fe39a44a), and this PR makes the boundary more visible. It may be beneficial to clarify this abstraction going forward.

From a directional and longer-term design point of view (not necessarily a requirement for this PR), one possible evolution could be:

  1. Move client authentication toward token-based credentials
    As a follow-up direction, the kbs-client could rely on tokens rather than private keys. For example, docker-compose or Kubernetes deployments could generate tokens at launch time and mount them at a well-defined path for the kbs-client to consume. This would be a change that we once talked about and can be considered independently of this PR.

  2. Conceptually split admin::validate_admin_token (the module's than the plugin's) into two stages

  • Token Verification / Identity Resolution: pluggable implementations that support bearer tokens, JWTs, or other credential formats, and return structured identity or claims.
  • Authorization Decision: a separate step that evaluates whether the resolved identity is allowed to access a given API.

This separation could exist conceptually first, even if the initial implementation remains simple.

  1. Introduce a simple, standalone authorization policy
    The authorization decision could be driven by a lightweight policy definition (e.g., file-based), instead of being embedded in startup configuration. KBS would still not perform identity issuance or user management; it would only verify externally issued credentials and apply a local authorization policy. More complex authentication or identity logic could remain out of band.

This direction certainly has drawbacks:

  1. Higher implementation complexity in the initial iteration.

  2. Increased learning cost for users, who would need to understand the policy model.
    That said, this could be mitigated by providing a default policy (e.g., allow_all) for simple use cases.

Overall, the current PR provides a practical and usable solution, while the points above are intended as considerations for future evolution rather than blockers for the current implementation.

wdyt?

Copy link
Member Author

@fitzthum fitzthum Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runtime configuration of admin roles is out of scope. Maybe sometime we could add an API to update the roles (and store them in the kv backend), but this is complicated, and currently we don't even have an admin login interface.

For 2, I will just rename the upper function to be more generic. We already have a split between checking the token, which is handled by the admin backends, and allowing access to the endpoint. It isn't perfect since the allow-all backend could be specified and roles could block things on top of that, but I've updated the docs to explain the different.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xynnn007 let's revisit this before the PR gets stale. i renamed a few things and updated the docs. I think it's best to use a simple approach for now. We have some internal people asking for this feature. I am hoping to get some feedback from them after we implement something and iterate if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this? I will do some changes upon your current PR after getting it merged. including moving key pair to token and some other changes about code structure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but what do you have in mind generally?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Almost the PR

Copy link
Member Author

@fitzthum fitzthum Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I have some comments about that PR but we can save for when it is actually posted. It seems fine in general although it's much more complex.

}
}
2 changes: 1 addition & 1 deletion kbs/src/admin/deny_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::admin::AdminBackend;
pub struct DenyAllBackend {}

impl AdminBackend for DenyAllBackend {
fn validate_admin_token(&self, _request: &HttpRequest) -> Result<()> {
fn validate_admin_token(&self, _request: &HttpRequest) -> Result<String> {
warn!("Admin endpoints are disabled");
Err(Error::AdminEndpointsDisabled)
}
Expand Down
6 changes: 6 additions & 0 deletions kbs/src/admin/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ pub enum Error {
#[error("Admin endpoints disabled.")]
AdminEndpointsDisabled,

#[error("Duplicate Admin Role")]
DuplicateAdminRole,

#[error("Invalid Regular Expression in Role")]
InvalidRoleRegex(#[from] regex::Error),

#[error("`auth_public_key` is not set in the config file")]
NoPublicKeyGiven,

Expand Down
178 changes: 165 additions & 13 deletions kbs/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use actix_web::HttpRequest;
use log::{info, warn};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;

pub mod allow_all;
Expand All @@ -21,9 +23,9 @@ use simple::{SimpleAdminBackend, SimpleAdminConfig};
#[derive(Clone)]
pub(crate) struct Admin {
backend: Arc<dyn AdminBackend>,
roles: HashMap<String, Regex>,
}

// create a simple backend
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum AdminBackendType {
Expand All @@ -45,6 +47,25 @@ impl Default for AdminBackendType {
pub struct AdminConfig {
#[serde(flatten)]
pub admin_backend: AdminBackendType,
/// Admin roles control which admin personas can access
/// which endpoints.
///
/// If no admin roles are specified, all admin will be able
/// to access all endpoints.
pub roles: Vec<AdminRole>,
}

/// An admin role is a rule that grants access for some roles to some endpoints.
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct AdminRole {
/// The admin role that this rule applies to.
/// The id is case insensitive.
pub id: String,
/// A regular expression selecting request paths this rule allows.
/// In other words, the paths that the above role can access.
#[serde(default)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means by default no endpoints can be accessed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That seems less error-prone.

pub allowed_endpoints: String,
}

impl TryFrom<AdminConfig> for Admin {
Expand All @@ -59,23 +80,47 @@ impl TryFrom<AdminConfig> for Admin {
AdminBackendType::DenyAll => Arc::new(DenyAllBackend::default()) as _,
};

Ok(Admin { backend })
// Parse roles to ensure valid regexes and no duplicates.
let mut roles = HashMap::new();
for role in value.roles {
let re = Regex::new(&role.allowed_endpoints)?;

if roles.insert(role.id.to_lowercase(), re).is_some() {
return Err(Error::DuplicateAdminRole);
}
}

Ok(Admin { backend, roles })
}
}

impl Admin {
pub fn validate_admin_token(&self, request: &HttpRequest) -> Result<()> {
let res = self.backend.validate_admin_token(request);
match res {
Ok(()) => info!("Allowing Admin access for {}", request.full_url().as_str()),
Err(ref e) => info!(
"Not allowing Admin access for {} due to: \n{}",
request.full_url().as_str(),
e
),
pub fn check_admin_access(&self, request: &HttpRequest) -> Result<()> {
if let Ok(role) = self.backend.validate_admin_token(request) {
info!("Admin Role: {role}");

// If there are no roles specified, allow all.
if self.roles.is_empty() {
info!(
"No admin roles configured. Allowing Request to {}",
request.full_url().as_str()
);
return Ok(());
}

if let Some(re) = self.roles.get(&role.to_lowercase()) {
if re.is_match(&request.uri().to_string()) {
info!("Allowing Request to {}", request.full_url().as_str());
return Ok(());
}
}
}

res
info!(
"Not allowing Admin access to {}",
request.full_url().as_str()
);
Err(Error::AdminAccessDenied)
}
}

Expand All @@ -85,5 +130,112 @@ pub(crate) trait AdminBackend: Send + Sync {
/// When a request is made to an admin endpoint, this method should be called
/// to validate that the user making the request is authorized
/// to access admin functionality.
fn validate_admin_token(&self, request: &HttpRequest) -> Result<()>;
///
/// If the token is valid, the backend will return an admin role.
fn validate_admin_token(&self, request: &HttpRequest) -> Result<String>;
}

#[cfg(test)]
mod tests {

use super::*;
use serde_json::json;

#[test]
pub fn make_admin_object() {
// basic (backwards compatible)
let admin_config_json = json!({
"type": "InsecureAllowAll",
});

let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap();
let _admin = Admin::try_from(config).unwrap();

// with invalid role (wrong field name)
let admin_config_json = json!({
"type": "InsecureAllowAll",
"roles" : [{
"id": "Anonymous",
"allowed_paths": "xyz"
}]
});

let config = serde_json::from_value::<AdminConfig>(admin_config_json);
assert!(config.is_err());

// with invalid role (bad regex)
let admin_config_json = json!({
"type": "InsecureAllowAll",
"roles" : [{
"id": "Anonymous",
"allowed_endpoints": "(xyz"
}]
});

let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap();
let admin = Admin::try_from(config);
assert!(admin.is_err());

// with invalid role (duplicate role)
let admin_config_json = json!({
"type": "InsecureAllowAll",
"roles" : [{
"id": "Anonymous",
"allowed_endpoints": "xyz"
},
{
"id": "Anonymous",
"allowed_endpoints": "abc"
}]
});

let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap();
let admin = Admin::try_from(config);
assert!(admin.is_err());
}

#[test]
pub fn check_requests() {
let admin_config_json = json!({
"type": "InsecureAllowAll",
"roles" : [{
"id": "Anonymous",
"allowed_endpoints": "^/resource/a/.+/c$"
}]
});

let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap();
let admin = Admin::try_from(config).unwrap();

// valid request
let req = actix_web::test::TestRequest::post()
.uri("/resource/a/b/c")
.to_http_request();
admin.check_admin_access(&req).unwrap();

// invalid request
let req = actix_web::test::TestRequest::post()
.uri("/resource/b/b/c")
.to_http_request();
assert!(admin.check_admin_access(&req).is_err());
}

#[test]
pub fn check_requests_wrong_role() {
let admin_config_json = json!({
"type": "InsecureAllowAll",
"roles" : [{
"id": "Steve",
"allowed_endpoints": "^/resource/a/.+/c$"
}]
});

let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap();
let admin = Admin::try_from(config).unwrap();

let req = actix_web::test::TestRequest::post()
.uri("/resource/a/b/c")
.to_http_request();
assert!(admin.check_admin_access(&req).is_err());
}
}
15 changes: 5 additions & 10 deletions kbs/src/admin/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ impl SimpleAdminBackend {
}

impl AdminBackend for SimpleAdminBackend {
fn validate_admin_token(&self, request: &HttpRequest) -> Result<()> {
let mut token_validated = false;

fn validate_admin_token(&self, request: &HttpRequest) -> Result<String> {
let bearer = Authorization::<Bearer>::parse(request)?.into_scheme();
let token = bearer.token();

Expand All @@ -68,20 +66,17 @@ impl AdminBackend for SimpleAdminBackend {
.verify_token::<NoCustomClaims>(token, Some(VerificationOptions::default()));
match res {
Ok(_claims) => {
token_validated = true;
info!("Admin access granted for {}", persona.id);
break;

// Return the first matching persona
return Ok(persona.id.clone());
}
Err(e) => {
info!("Access not granted for {} due to: \n{}", persona.id, e);
}
}
}

if !token_validated {
Err(Error::AdminAccessDenied)
} else {
Ok(())
}
Err(Error::AdminAccessDenied)
}
}
Loading
Loading