Skip to content

Commit ab729cb

Browse files
fix(agent): remove optional agent name from installer side (#1808)
1 parent 0bd32c2 commit ab729cb

21 files changed

Lines changed: 127 additions & 319 deletions

File tree

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,7 @@ pub fn any_kdc_claims(now: i64, validity_duration: i64) -> impl Strategy<Value =
323323
#[derive(Debug, Clone, Serialize)]
324324
pub struct EnrollmentClaims {
325325
pub jet_gw_url: String,
326-
#[serde(skip_serializing_if = "Option::is_none")]
327-
pub jet_agent_name: Option<String>,
326+
pub jet_agent_name: String,
328327
pub nbf: i64,
329328
pub exp: i64,
330329
pub jti: Uuid,
@@ -333,7 +332,7 @@ pub struct EnrollmentClaims {
333332
pub fn any_enrollment_claims(now: i64, validity_duration: i64) -> impl Strategy<Value = EnrollmentClaims> {
334333
(
335334
"https://[a-z]{1,10}\\.[a-z]{1,5}(:[0-9]{3,4})?",
336-
option::of("[a-zA-Z0-9_-]{1,25}"),
335+
"[a-zA-Z0-9_-]{1,25}",
337336
uuid_typed(),
338337
)
339338
.prop_map(move |(jet_gw_url, jet_agent_name, jti)| EnrollmentClaims {

devolutions-agent/src/enrollment.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ use crate::config;
2222
pub struct EnrollmentJwtClaims {
2323
/// Gateway URL to connect to for enrollment.
2424
pub jet_gw_url: String,
25-
/// Suggested agent display name (optional hint).
26-
#[serde(default)]
27-
pub jet_agent_name: Option<String>,
25+
/// Agent friendly name.
26+
pub jet_agent_name: String,
2827
}
2928

3029
/// Decode an enrollment JWT to extract agent-side configuration claims.
@@ -61,8 +60,6 @@ pub fn parse_enrollment_jwt(jwt: &str) -> Result<EnrollmentJwtClaims> {
6160
struct EnrollRequest {
6261
/// Agent-generated UUID (the agent owns its identity)
6362
agent_id: Uuid,
64-
/// Friendly name for the agent
65-
agent_name: String,
6663
/// PEM-encoded Certificate Signing Request
6764
csr_pem: String,
6865
/// Optional hostname of the agent machine (added as DNS SAN in the issued certificate)
@@ -94,19 +91,24 @@ pub struct PersistedEnrollment {
9491
///
9592
/// # Arguments
9693
/// * `gateway_url` - Base Gateway URL (e.g., "https://gateway.example.com:7171")
97-
/// * `enrollment_token` - JWT token for enrollment
98-
/// * `agent_name` - Friendly name for this agent
94+
/// * `enrollment_token` - JWT token for enrollment. The signed `jet_gw_url`
95+
/// claim is also used by `up --enrollment-string` when no explicit gateway
96+
/// URL is provided. The signed `jet_agent_name` claim is the enrollment name.
9997
/// * `advertise_subnets` - List of subnets to advertise (e.g., ["10.0.0.0/8"])
10098
pub async fn enroll_agent(
10199
gateway_url: &str,
102100
enrollment_token: &str,
103-
agent_name: &str,
104101
advertise_subnets: Vec<String>,
105102
) -> Result<PersistedEnrollment> {
103+
let EnrollmentJwtClaims {
104+
jet_agent_name: agent_name,
105+
..
106+
} = parse_enrollment_jwt(enrollment_token)?;
107+
106108
// Generate key pair and CSR locally — the private key never leaves this machine.
107-
let (key_pem, csr_pem) = generate_key_and_csr(agent_name)?;
109+
let (key_pem, csr_pem) = generate_key_and_csr(&agent_name)?;
108110

109-
let enroll_response = request_enrollment(gateway_url, enrollment_token, agent_name, &csr_pem).await?;
111+
let enroll_response = request_enrollment(gateway_url, enrollment_token, &csr_pem).await?;
110112
persist_enrollment_response(agent_name, advertise_subnets, enroll_response, &key_pem)
111113
}
112114

@@ -127,12 +129,7 @@ fn generate_key_and_csr(agent_name: &str) -> Result<(String, String)> {
127129
Ok((key_pem, csr_pem))
128130
}
129131

130-
async fn request_enrollment(
131-
gateway_url: &str,
132-
enrollment_token: &str,
133-
agent_name: &str,
134-
csr_pem: &str,
135-
) -> Result<EnrollResponse> {
132+
async fn request_enrollment(gateway_url: &str, enrollment_token: &str, csr_pem: &str) -> Result<EnrollResponse> {
136133
let client = reqwest::Client::new();
137134
let enroll_url = format!("{}/jet/tunnel/enroll", gateway_url.trim_end_matches('/'));
138135

@@ -141,7 +138,6 @@ async fn request_enrollment(
141138
.bearer_auth(enrollment_token)
142139
.json(&EnrollRequest {
143140
agent_id: Uuid::new_v4(),
144-
agent_name: agent_name.to_owned(),
145141
csr_pem: csr_pem.to_owned(),
146142
agent_hostname: hostname::get()
147143
.ok()
@@ -162,7 +158,7 @@ async fn request_enrollment(
162158
}
163159

164160
fn persist_enrollment_response(
165-
agent_name: &str,
161+
agent_name: String,
166162
advertise_subnets: Vec<String>,
167163
EnrollResponse {
168164
agent_id,
@@ -284,7 +280,7 @@ fn persist_enrollment_response(
284280

285281
Ok(PersistedEnrollment {
286282
agent_id,
287-
agent_name: agent_name.to_owned(),
283+
agent_name,
288284
client_cert_path,
289285
client_key_path,
290286
gateway_ca_path,
@@ -325,7 +321,7 @@ pub fn is_cert_expiring(cert_path: &Utf8Path, threshold_days: u32) -> Result<boo
325321

326322
/// Extract the `CommonName` from an existing PEM certificate. The renewal CSR
327323
/// must reuse the agent's name across renewals — the gateway looks the agent
328-
/// up in its registry by that name, and the most authoritative source for it
324+
/// up in its registry by that name, and the source for it
329325
/// is the cert the gateway itself signed last time.
330326
pub fn read_agent_name_from_cert(cert_path: &Utf8Path) -> Result<String> {
331327
use std::io::BufReader;
@@ -395,4 +391,12 @@ mod tests {
395391
}));
396392
assert!(parse_enrollment_jwt(&jwt).is_err());
397393
}
394+
395+
#[test]
396+
fn parse_enrollment_jwt_requires_agent_name() {
397+
let jwt = make_jwt(serde_json::json!({
398+
"jet_gw_url": "https://gw.example.com",
399+
}));
400+
assert!(parse_enrollment_jwt(&jwt).is_err());
401+
}
398402
}

devolutions-agent/src/main.rs

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ const START_FAILED_ERR_CODE: u32 = 2;
5555
struct UpCommand {
5656
gateway_url: String,
5757
enrollment_token: String,
58-
agent_name: String,
5958
advertise_subnets: Vec<String>,
6059
}
6160

@@ -149,8 +148,6 @@ fn parse_up_command_args(args: &[String]) -> Result<UpCommand> {
149148

150149
fn parse_up_command_args_with_reader<R: BufRead>(args: &[String], mut stdin_reader: R) -> Result<UpCommand> {
151150
let mut gateway_url = None;
152-
let mut enrollment_token = None;
153-
let mut agent_name = None;
154151
let mut enrollment_string = None;
155152
let mut advertise_subnets = Vec::new();
156153

@@ -160,8 +157,6 @@ fn parse_up_command_args_with_reader<R: BufRead>(args: &[String], mut stdin_read
160157

161158
match arg {
162159
"--gateway" => gateway_url = Some(parse_required_value(args, &mut index, "--gateway")?),
163-
"--token" | "--enrollment-token" => enrollment_token = Some(parse_required_value(args, &mut index, arg)?),
164-
"--name" | "--agent-name" => agent_name = Some(parse_required_value(args, &mut index, arg)?),
165160
"--enrollment-string" => enrollment_string = Some(parse_required_value(args, &mut index, arg)?),
166161
"--advertise-routes" | "--advertise-subnets" => {
167162
advertise_subnets.extend(parse_advertise_subnets(&parse_required_value(args, &mut index, arg)?))
@@ -172,37 +167,29 @@ fn parse_up_command_args_with_reader<R: BufRead>(args: &[String], mut stdin_read
172167
index += 1;
173168
}
174169

175-
if let Some(enrollment_string) = enrollment_string {
176-
// A single hyphen means "read the enrollment string from stdin".
177-
let enrollment_string = if enrollment_string == "-" {
178-
let mut line = String::new();
179-
stdin_reader
180-
.read_line(&mut line)
181-
.context("failed to read enrollment string from stdin")?;
182-
let trimmed = line.trim().to_owned();
183-
if trimmed.is_empty() {
184-
bail!("enrollment string read from stdin is empty");
185-
}
186-
trimmed
187-
} else {
188-
enrollment_string
189-
};
190-
191-
let claims = parse_enrollment_jwt(&enrollment_string)?;
192-
193-
// The JWT itself is the Bearer token; the Gateway verifies the signature.
194-
gateway_url.get_or_insert(claims.jet_gw_url);
195-
enrollment_token.get_or_insert(enrollment_string);
196-
197-
if agent_name.is_none() {
198-
agent_name = claims.jet_agent_name;
170+
let enrollment_string = enrollment_string.context("missing required --enrollment-string")?;
171+
172+
// A single hyphen means "read the enrollment string from stdin".
173+
let enrollment_token = if enrollment_string == "-" {
174+
let mut line = String::new();
175+
stdin_reader
176+
.read_line(&mut line)
177+
.context("failed to read enrollment string from stdin")?;
178+
let trimmed = line.trim().to_owned();
179+
if trimmed.is_empty() {
180+
bail!("enrollment string read from stdin is empty");
199181
}
200-
}
182+
trimmed
183+
} else {
184+
enrollment_string
185+
};
186+
187+
let claims = parse_enrollment_jwt(&enrollment_token)?;
188+
gateway_url.get_or_insert(claims.jet_gw_url);
201189

202190
Ok(UpCommand {
203191
gateway_url: gateway_url.context("missing required --gateway")?,
204-
enrollment_token: enrollment_token.context("missing required --token")?,
205-
agent_name: agent_name.context("missing required --name")?,
192+
enrollment_token,
206193
advertise_subnets,
207194
})
208195
}
@@ -253,9 +240,8 @@ fn main() {
253240
let gateway_url = env::args()
254241
.nth(2)
255242
.expect("missing gateway URL (e.g., https://gateway.example.com:7171)");
256-
let enrollment_token = env::args().nth(3).expect("missing enrollment token");
257-
let agent_name = env::args().nth(4).expect("missing agent name");
258-
let subnets_arg = env::args().nth(5).unwrap_or_default();
243+
let enrollment_token = env::args().nth(3).expect("missing enrollment string");
244+
let subnets_arg = env::args().nth(4).unwrap_or_default();
259245

260246
let advertise_subnets: Vec<String> = if subnets_arg.is_empty() {
261247
Vec::new()
@@ -265,13 +251,9 @@ fn main() {
265251

266252
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
267253
rt.block_on(async {
268-
if let Err(e) = devolutions_agent::enrollment::enroll_agent(
269-
&gateway_url,
270-
&enrollment_token,
271-
&agent_name,
272-
advertise_subnets,
273-
)
274-
.await
254+
if let Err(e) =
255+
devolutions_agent::enrollment::enroll_agent(&gateway_url, &enrollment_token, advertise_subnets)
256+
.await
275257
{
276258
eprintln!("[ERROR] Enrollment failed: {e:#}");
277259
std::process::exit(1);
@@ -293,7 +275,6 @@ fn main() {
293275
devolutions_agent::enrollment::enroll_agent(
294276
&command.gateway_url,
295277
&command.enrollment_token,
296-
&command.agent_name,
297278
command.advertise_subnets,
298279
)
299280
.await
@@ -320,14 +301,16 @@ mod tests {
320301
use super::*;
321302

322303
#[test]
323-
fn parse_up_command_args_uses_default_config_path() {
304+
fn parse_up_command_args_accepts_advertise_routes() {
305+
let jwt = make_jwt(serde_json::json!({
306+
"exp": 1_999_999_999i64,
307+
"jti": "00000000-0000-0000-0000-000000000000",
308+
"jet_gw_url": "https://gateway.example.com:7171",
309+
"jet_agent_name": "site-a-agent",
310+
}));
324311
let args = vec![
325-
"--gateway".to_owned(),
326-
"https://gateway.example.com:7171".to_owned(),
327-
"--token".to_owned(),
328-
"bootstrap-token".to_owned(),
329-
"--name".to_owned(),
330-
"site-a-agent".to_owned(),
312+
"--enrollment-string".to_owned(),
313+
jwt.clone(),
331314
"--advertise-routes".to_owned(),
332315
"10.0.0.0/8,192.168.1.0/24".to_owned(),
333316
];
@@ -338,22 +321,25 @@ mod tests {
338321
parsed,
339322
UpCommand {
340323
gateway_url: "https://gateway.example.com:7171".to_owned(),
341-
enrollment_token: "bootstrap-token".to_owned(),
342-
agent_name: "site-a-agent".to_owned(),
324+
enrollment_token: jwt,
343325
advertise_subnets: vec!["10.0.0.0/8".to_owned(), "192.168.1.0/24".to_owned()],
344326
}
345327
);
346328
}
347329

348330
#[test]
349-
fn parse_up_command_args_accepts_aliases() {
331+
fn parse_up_command_args_accepts_advertise_subnets_alias() {
332+
let jwt = make_jwt(serde_json::json!({
333+
"exp": 1_999_999_999i64,
334+
"jti": "00000000-0000-0000-0000-000000000000",
335+
"jet_gw_url": "https://gateway.example.com:7171",
336+
"jet_agent_name": "site-a-agent",
337+
}));
350338
let args = vec![
351339
"--gateway".to_owned(),
352340
"https://gateway.example.com:7171".to_owned(),
353-
"--enrollment-token".to_owned(),
354-
"bootstrap-token".to_owned(),
355-
"--agent-name".to_owned(),
356-
"site-a-agent".to_owned(),
341+
"--enrollment-string".to_owned(),
342+
jwt,
357343
"--advertise-subnets".to_owned(),
358344
"10.0.0.0/8".to_owned(),
359345
];
@@ -391,6 +377,22 @@ mod tests {
391377
assert_eq!(parsed.gateway_url, "https://gateway.example.com:7171");
392378
// The JWT itself is used as the Bearer token for /jet/tunnel/enroll.
393379
assert_eq!(parsed.enrollment_token, jwt);
394-
assert_eq!(parsed.agent_name, "site-a-agent");
380+
}
381+
382+
#[test]
383+
fn parse_up_command_args_rejects_split_inputs() {
384+
for flag in ["--name", "--agent-name", "--token", "--enrollment-token"] {
385+
let args = vec![flag.to_owned(), "site-a-agent".to_owned()];
386+
let error = parse_up_command_args(&args).expect_err("argument should be rejected");
387+
388+
assert!(error.to_string().contains("unknown argument"));
389+
}
390+
}
391+
392+
#[test]
393+
fn parse_up_command_args_requires_enrollment_string() {
394+
let args = Vec::new();
395+
396+
assert!(parse_up_command_args(&args).is_err());
395397
}
396398
}

devolutions-gateway/src/api/tunnel.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use crate::http::HttpError;
1111
pub struct EnrollRequest {
1212
/// Agent-generated UUID (the agent owns its identity).
1313
pub agent_id: Uuid,
14-
/// Friendly name for the agent.
15-
pub agent_name: String,
1614
/// PEM-encoded Certificate Signing Request from the agent.
1715
pub csr_pem: String,
1816
/// Optional hostname of the agent machine (added as DNS SAN in the issued certificate).
@@ -46,24 +44,25 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
4644
/// Enroll a new agent.
4745
///
4846
/// Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key
49-
/// (e.g. DVLS, Hub, or any PEM service).
47+
/// (e.g. DVLS, Hub, PAM service, or any other compatible provisioner).
5048
///
5149
/// The agent generates its own key pair and sends a CSR. The gateway signs it
5250
/// and returns the certificate. The private key never leaves the agent.
5351
async fn enroll_agent(
54-
_: crate::extract::EnrollmentToken,
52+
crate::extract::EnrollmentToken(token_claims): crate::extract::EnrollmentToken,
5553
State(DgwState {
5654
conf_handle,
5755
agent_tunnel_handle,
5856
..
5957
}): State<DgwState>,
6058
Json(EnrollRequest {
6159
agent_id,
62-
agent_name,
6360
csr_pem,
6461
agent_hostname,
6562
}): Json<EnrollRequest>,
6663
) -> Result<Json<EnrollResponse>, HttpError> {
64+
let agent_name = token_claims.jet_agent_name;
65+
6766
// Validate agent name: 1-255 printable ASCII characters.
6867
if agent_name.is_empty() || 255 < agent_name.len() || agent_name.bytes().any(|b| !(0x20..=0x7E).contains(&b)) {
6968
return Err(HttpError::bad_request().msg("agent name must be 1-255 printable ASCII characters"));

devolutions-gateway/src/token.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -499,12 +499,10 @@ pub struct ScopeTokenClaims {
499499

500500
/// Claims carried by an agent-tunnel enrollment JWT.
501501
///
502-
/// The JWT itself is copy-pasted by the operator into the agent's
503-
/// `--enrollment-string` argument. Agent reads `jet_gw_url` and
504-
/// `jet_agent_name` locally (without verifying the signature, since it is
505-
/// the intended recipient), then sends the JWT as the Bearer token to
506-
/// `/jet/tunnel/enroll`, where the Gateway verifies the signature, content
507-
/// type, and expiry against the configured provisioner key.
502+
/// The agent reads `jet_gw_url` and `jet_agent_name` locally, then sends
503+
/// the JWT as the Bearer token to `/jet/tunnel/enroll`, where the Gateway
504+
/// verifies the signature, content type, expiry, and agent name against
505+
/// the configured provisioner key.
508506
#[derive(Debug, Clone, Serialize, Deserialize)]
509507
pub struct EnrollmentTokenClaims {
510508
/// JWT expiration time claim.
@@ -516,9 +514,8 @@ pub struct EnrollmentTokenClaims {
516514
/// Gateway URL the agent should connect to for enrollment.
517515
pub jet_gw_url: String,
518516

519-
/// Suggested agent display name (optional hint).
520-
#[serde(default)]
521-
pub jet_agent_name: Option<String>,
517+
/// Agent friendly name.
518+
pub jet_agent_name: String,
522519
}
523520

524521
// ----- bridge claims ----- //

0 commit comments

Comments
 (0)