Skip to content

Commit 448d477

Browse files
fix(agent): improve enrollment token validation (#1806)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent b3ab657 commit 448d477

17 files changed

Lines changed: 197 additions & 311 deletions

File tree

crates/devolutions-gateway-generators/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub fn token_content_type() -> impl Strategy<Value = token::ContentType> {
2525
Just(token::ContentType::Jmux),
2626
Just(token::ContentType::Kdc),
2727
Just(token::ContentType::Jrl),
28+
Just(token::ContentType::Enrollment),
2829
]
2930
.no_shrink()
3031
}
@@ -319,6 +320,31 @@ pub fn any_kdc_claims(now: i64, validity_duration: i64) -> impl Strategy<Value =
319320
})
320321
}
321322

323+
#[derive(Debug, Clone, Serialize)]
324+
pub struct EnrollmentClaims {
325+
pub jet_gw_url: String,
326+
#[serde(skip_serializing_if = "Option::is_none")]
327+
pub jet_agent_name: Option<String>,
328+
pub nbf: i64,
329+
pub exp: i64,
330+
pub jti: Uuid,
331+
}
332+
333+
pub fn any_enrollment_claims(now: i64, validity_duration: i64) -> impl Strategy<Value = EnrollmentClaims> {
334+
(
335+
"https://[a-z]{1,10}\\.[a-z]{1,5}(:[0-9]{3,4})?",
336+
option::of("[a-zA-Z0-9_-]{1,25}"),
337+
uuid_typed(),
338+
)
339+
.prop_map(move |(jet_gw_url, jet_agent_name, jti)| EnrollmentClaims {
340+
jet_gw_url,
341+
jet_agent_name,
342+
jti,
343+
nbf: now,
344+
exp: now + validity_duration,
345+
})
346+
}
347+
322348
#[derive(Debug, Serialize, Clone)]
323349
#[serde(untagged)]
324350
pub enum TokenClaims {
@@ -327,6 +353,7 @@ pub enum TokenClaims {
327353
Bridge(BridgeClaims),
328354
Jmux(JmuxClaims),
329355
Kdc(KdcClaims),
356+
Enrollment(EnrollmentClaims),
330357
}
331358

332359
impl TokenClaims {
@@ -337,6 +364,7 @@ impl TokenClaims {
337364
TokenClaims::Bridge(_) => "BRIDGE",
338365
TokenClaims::Jmux(_) => "JMUX",
339366
TokenClaims::Kdc(_) => "KDC",
367+
TokenClaims::Enrollment(_) => "ENROLLMENT",
340368
}
341369
}
342370

@@ -355,6 +383,7 @@ pub fn any_claims_with_validity_duration(now: i64, validity_duration: i64) -> im
355383
any_kdc_claims(now, validity_duration).prop_map(TokenClaims::Kdc),
356384
any_jmux_claims(now, validity_duration).prop_map(TokenClaims::Jmux),
357385
any_association_claims(now, validity_duration).prop_map(TokenClaims::Association),
386+
any_enrollment_claims(now, validity_duration).prop_map(TokenClaims::Enrollment),
358387
]
359388
}
360389

devolutions-agent/src/enrollment.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::config;
1717
/// the JWT (the operator). The Gateway verifies the signature when the JWT is
1818
/// presented as the Bearer token on `/jet/tunnel/enroll`.
1919
///
20-
/// Additional standard claims (`exp`, `jti`, `scope`, ...) are ignored here.
20+
/// Additional standard claims (`exp`, `jti`, ...) are ignored here.
2121
#[derive(Debug, serde::Deserialize)]
2222
pub struct EnrollmentJwtClaims {
2323
/// Gateway URL to connect to for enrollment.
@@ -371,7 +371,7 @@ mod tests {
371371
/// Build a JWT with arbitrary header/signature placeholders. The parser never
372372
/// verifies the signature, so the content of those two segments is irrelevant.
373373
fn make_jwt(payload: serde_json::Value) -> String {
374-
let header = serde_json::json!({ "alg": "RS256", "typ": "JWT" });
374+
let header = serde_json::json!({ "alg": "RS256", "typ": "JWT", "cty": "ENROLLMENT" });
375375
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
376376
format!(
377377
"{}.{}.{}",
@@ -391,7 +391,6 @@ mod tests {
391391
#[test]
392392
fn parse_enrollment_jwt_requires_gw_url() {
393393
let jwt = make_jwt(serde_json::json!({
394-
"scope": "gateway.tunnel.enroll",
395394
"jet_agent_name": "agent-a",
396395
}));
397396
assert!(parse_enrollment_jwt(&jwt).is_err());

devolutions-agent/src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ mod tests {
366366
/// Build a JWT with the given payload. The header and signature are placeholders —
367367
/// the agent does not verify them; only the Gateway does.
368368
fn make_jwt(payload: serde_json::Value) -> String {
369-
let header = serde_json::json!({ "alg": "RS256", "typ": "JWT" });
369+
let header = serde_json::json!({ "alg": "RS256", "typ": "JWT", "cty": "ENROLLMENT" });
370370
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
371371
format!(
372372
"{}.{}.{}",
@@ -379,7 +379,6 @@ mod tests {
379379
#[test]
380380
fn parse_up_command_args_accepts_enrollment_string() {
381381
let jwt = make_jwt(serde_json::json!({
382-
"scope": "gateway.tunnel.enroll",
383382
"exp": 1_999_999_999i64,
384383
"jti": "00000000-0000-0000-0000-000000000000",
385384
"jet_gw_url": "https://gateway.example.com:7171",

devolutions-gateway/openapi/gateway-api.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,7 @@ components:
11531153
- gateway.traffic.ack
11541154
- gateway.net.monitor.config
11551155
- gateway.net.monitor.drain
1156-
- gateway.agent.enroll
1156+
- gateway.agent.delete
11571157
- gateway.agent.read
11581158
AckRequest:
11591159
type: object

0 commit comments

Comments
 (0)