Skip to content

Commit cc28e35

Browse files
committed
More access fixes and bounds
1 parent 224594e commit cc28e35

6 files changed

Lines changed: 103 additions & 47 deletions

File tree

api/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub trait SushApi {
8989
///
9090
/// Since there may be only one session active on the rack at a time
9191
/// and we do not want to prevent starting new sessions, anyone is
92-
/// allowed to stop anyone else's sesssion and start their own.
92+
/// allowed to stop anyone else's session and start their own.
9393
#[endpoint { method = POST, path = "/sessions/{session_id}/stop" }]
9494
async fn session_stop(
9595
ctx: RequestContext<Self::Context>,

client/src/commands.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,9 @@ async fn job_start(
859859
start_args: JobStartArgs,
860860
) -> Result<(), CommandError> {
861861
let Some(identity) = ctx.get_identity() else {
862+
// It's ok to bail here because if we haven't got an identity in
863+
// the context, we also won't have a session; `job start` is only
864+
// useful from the REPL.
862865
return Err(CommandError::InvalidAuthorization);
863866
};
864867
let authz = identity.into_credentials().to_string();

server/src/error.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ pub enum JobError {
7777
Slice(#[from] std::array::TryFromSliceError),
7878
#[error(transparent)]
7979
Task(#[from] tokio::task::JoinError),
80+
#[error("Too many certificates")]
81+
TooManyCerts(usize),
82+
#[error("Too many identities")]
83+
TooManyIdentities(usize),
84+
#[error("Too many jobs in a session: {0}")]
85+
TooManyJobs(usize),
8086
#[error("Unauthorized request")]
8187
Unauthorized(Nonce),
8288
#[error("Unable to wait for job end")]
@@ -174,7 +180,10 @@ impl From<JobError> for HttpError {
174180
| PublicKeyRevoked { .. }
175181
| SessionNotFound(_)
176182
| SessionWrongIdentity
177-
| OutputPending => {
183+
| OutputPending
184+
| TooManyCerts(_)
185+
| TooManyIdentities(_)
186+
| TooManyJobs(_) => {
178187
HttpError::for_client_error(None, ClientErrorStatusCode::BAD_REQUEST, message)
179188
}
180189
}

server/src/manager.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,23 @@ pub const ROOT_CERTS: &[&[u8]] = &[
5454
/// Maximum certificate chain length.
5555
const MAX_CERT_CHAIN_LEN: usize = 10;
5656

57+
/// Maximum number of job signing certificates.
58+
/// The ideal number of certs is 1, so this need not be large.
59+
const MAX_CERTS: usize = 100;
60+
61+
/// Maximum number of identities.
62+
const MAX_IDENTITIES: usize = 1_000;
63+
64+
/// Maximum number of jobs in a session. Should be set to a large
65+
/// but reasonable value; big enough to not be annoying, but small
66+
/// enough to avoid filling up memory.
67+
const MAX_JOBS: usize = 10_000;
68+
5769
/// Maximum number of outstanding authentication nonces.
58-
const MAX_OUTSTANDING_NONCES: NonZeroUsize = NonZeroUsize::new(1000).unwrap();
70+
/// We do not really expect more than one simultaneous user,
71+
/// nor do we expect hostile (DoS) requests, so a small value
72+
/// here is adequate.
73+
const MAX_OUTSTANDING_NONCES: NonZeroUsize = NonZeroUsize::new(100).unwrap();
5974

6075
/// Output files or ranges larger than this will not be served all at once.
6176
const OUTPUT_THRESHOLD: u64 = ByteSize::mb(128).as_u64();
@@ -126,6 +141,9 @@ impl JobManager {
126141

127142
fn import_cert_inner(&self, cert: Certificate, root_allowed: bool) -> Result<KeyId, JobError> {
128143
let mut certs = self.certs.lock().unwrap();
144+
if certs.len() == MAX_CERTS {
145+
return Err(JobError::TooManyCerts(MAX_CERTS));
146+
}
129147

130148
// Verify the certificate signature.
131149
let signature = Signature::try_from(&cert)?;
@@ -235,12 +253,14 @@ impl JobManager {
235253
Some(
236254
identity @ Identity {
237255
key_id: identity_key_id,
256+
nonce: n,
238257
cnonce: c,
239258
signature: s,
240259
time_revoked: None,
241260
..
242261
},
243262
) if try_authn!(identity.is_still_valid(&now), "credentials expired")
263+
&& try_authn!(n == nonce, "invalid nonce")
244264
&& try_authn!(c == cnonce, "invalid cnonce")
245265
&& try_authn!(s == signature, "invalid signature") =>
246266
{
@@ -258,6 +278,9 @@ impl JobManager {
258278
"invalid public key"
259279
) =>
260280
{
281+
if identities.len() == MAX_IDENTITIES {
282+
return Err(JobError::TooManyIdentities(MAX_IDENTITIES));
283+
}
261284
let public_key = public_key.expect("checked in guard");
262285
let response = credentials.clone().into_challenge_response();
263286
let verified = try_authn!(response.verify_with_ssh_public_key(&public_key))?;
@@ -330,9 +353,15 @@ impl JobManager {
330353
session_id: &SessionId,
331354
) -> Result<(), JobError> {
332355
// Anyone is allowed to stop a session.
333-
if let Some(session) = { self.session.lock().unwrap().take() }
334-
&& session.session_id() == session_id
335-
{
356+
let session = {
357+
let mut session_guard = self.session.lock().unwrap();
358+
if session_guard.as_ref().map(|s| s.session_id()) == Some(session_id) {
359+
session_guard.take()
360+
} else {
361+
None
362+
}
363+
};
364+
if let Some(session) = session {
336365
self.session_stop_inner(session).await
337366
} else {
338367
Err(JobError::SessionNotFound(session_id.to_owned()))
@@ -366,8 +395,14 @@ impl JobManager {
366395
let wait = params.wait;
367396
let started = {
368397
let job_id = job.job_id().to_owned();
369-
if self.jobs.lock().unwrap().contains_key(&job_id) {
370-
return Err(JobError::InvalidJobId(job_id));
398+
{
399+
let jobs = self.jobs.lock().unwrap();
400+
if jobs.contains_key(&job_id) {
401+
return Err(JobError::InvalidJobId(job_id));
402+
}
403+
if jobs.len() == MAX_JOBS {
404+
return Err(JobError::TooManyJobs(MAX_JOBS));
405+
}
371406
}
372407

373408
let cert_key_id = job.key_id().to_owned();
@@ -410,20 +445,21 @@ impl JobManager {
410445
if wait { Ok(Some(rx.await?)) } else { Ok(None) }
411446
}
412447

448+
/// For jobs in the current session, only the owner is allowed access.
449+
/// For jobs from previous sessions, anyone may access them; otherwise,
450+
/// they would be orphaned.
413451
fn check_job_owner(&self, authn: &Identity, job_id: &JobId) -> Result<(), JobError> {
414-
let Some(ref session) = *self.session.lock().unwrap() else {
415-
return Err(JobError::NoSession);
416-
};
417452
let Some(job_session_id) = self.job_status(authn, job_id)?.session_id() else {
418453
return Err(JobError::JobNotFound(job_id.to_owned()));
419454
};
420-
if job_session_id != *session.session_id() {
421-
return Err(JobError::JobNotFound(job_id.to_owned()));
422-
}
423-
if session.key_id() != Some(&authn.key_id) {
424-
return Err(JobError::SessionWrongIdentity);
455+
if let Some(session) = self.session.lock().unwrap().as_ref()
456+
&& job_session_id == *session.session_id()
457+
&& session.key_id() != Some(&authn.key_id)
458+
{
459+
Err(JobError::SessionWrongIdentity)
460+
} else {
461+
Ok(())
425462
}
426-
Ok(())
427463
}
428464

429465
pub async fn job_start_interactive_session(
@@ -458,12 +494,12 @@ impl JobManager {
458494

459495
pub async fn job_output(
460496
&self,
461-
_authn: &Identity,
497+
authn: &Identity,
462498
job_id: &JobId,
463499
stream: JobOutputStream,
464500
range: Option<Range>,
465501
) -> Result<Vec<u8>, JobError> {
466-
// Anyone is allowed to read job output.
502+
self.check_job_owner(authn, job_id)?;
467503
get_job_output(&self.output_dir, job_id, stream, range)
468504
}
469505

server/src/monitor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl MonitorRequest {
265265
}
266266

267267
/// Event representing the beginning of a job. The manager spawns the child,
268-
/// then passses one of these to the monitor.
268+
/// then passes one of these to the monitor.
269269
#[derive(Debug)]
270270
pub struct JobStarted {
271271
pub job: VerifiedJob,

sush.json

Lines changed: 35 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)