Skip to content

Commit

Permalink
Add option to suppress error messages if not authenticated
Browse files Browse the repository at this point in the history
  • Loading branch information
mbuesch committed Nov 9, 2024
1 parent dc3d36c commit b34de0f
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 3 deletions.
22 changes: 22 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ Otherwise the authentication handshake will fail over very slow network connecti

This option defaults to `control-timeout=5.0` seconds, if it is absent from the configuration.

### `control-error-policy`

The `control-error-policy` error policy specifies how to communicate control protocol errors to the client.

If the policy is set to `always`, then error messages will always be transmitted to the connected client.

If the policy is set to `basic-auth`, then error messages are suppressed unless the connected client has passed basic authentication.

If the policy is set to `full-auth`, then error messages are suppressed unless the connected client has passed full authentication.

Setting the policy to `basic-auth` or `full-auth` means that unauthenticated clients don't get error responses.
This helps to not reveal what service is running on the control port to malicious scanner clients.
This enables a more stealth operation of the server.

The disadvantage of setting this to anything but `always` is that legitimate clients might not always receive a proper error message and end up in a network timeout instead.

Possible values: always, basic-auth, full-auth

The recommended value is: basic-auth

This option defaults to `control-error-policy=always`, if it is absent from the configuration.

### `seccomp`

The `seccomp` option turns [Seccomp](https://en.wikipedia.org/wiki/Seccomp) security hardening on or off.
Expand Down
49 changes: 47 additions & 2 deletions letmein-conf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ impl Resource {
}
}

/// Error reporting policy.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum ErrorPolicy {
/// Always report errors.
#[default]
Always,

/// Only report errors if basic authentication passed.
BasicAuth,

/// Only report errors if full authentication passed.
FullAuth,
}

/// Seccomp setting.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum Seccomp {
Expand Down Expand Up @@ -142,6 +156,21 @@ fn get_control_timeout(ini: &Ini) -> ah::Result<Duration> {
Ok(DEFAULT_CONTROL_TIMEOUT)
}

fn get_control_error_policy(ini: &Ini) -> ah::Result<ErrorPolicy> {
if let Some(policy) = ini.get("GENERAL", "control-error-policy") {
return match policy.to_lowercase().trim() {
"always" => Ok(ErrorPolicy::Always),
"basic-auth" => Ok(ErrorPolicy::BasicAuth),
"full-auth" => Ok(ErrorPolicy::FullAuth),
other => Err(err!(
"Config option 'control-error-policy = {other}' is not valid. \
Valid values are: always, basic-auth, full-auth."
)),
};
}
Ok(Default::default())
}

fn get_seccomp(ini: &Ini) -> ah::Result<Seccomp> {
if let Some(seccomp) = ini.get("GENERAL", "seccomp") {
return seccomp.parse();
Expand Down Expand Up @@ -335,6 +364,7 @@ pub struct Config {
debug: bool,
port: u16,
control_timeout: Duration,
control_error_policy: ErrorPolicy,
seccomp: Seccomp,
keys: HashMap<UserId, Key>,
resources: HashMap<ResourceId, Resource>,
Expand Down Expand Up @@ -412,6 +442,7 @@ impl Config {
let debug = get_debug(ini)?;
let port = get_port(ini)?;
let control_timeout = get_control_timeout(ini)?;
let control_error_policy = get_control_error_policy(ini)?;
let seccomp = get_seccomp(ini)?;
let keys = get_keys(ini)?;
let resources = get_resources(ini)?;
Expand All @@ -428,6 +459,7 @@ impl Config {
self.debug = debug;
self.port = port;
self.control_timeout = control_timeout;
self.control_error_policy = control_error_policy;
self.seccomp = seccomp;
self.keys = keys;
self.resources = resources;
Expand All @@ -449,10 +481,16 @@ impl Config {
self.port
}

/// Get the `control-timeout` option from `[GENERAL]` section.
pub fn control_timeout(&self) -> Duration {
self.control_timeout
}

/// Get the `control-error-policy` option from `[GENERAL]` section.
pub fn control_error_policy(&self) -> ErrorPolicy {
self.control_error_policy
}

/// Get the `seccomp` option from `[GENERAL]` section.
pub fn seccomp(&self) -> Seccomp {
self.seccomp
Expand Down Expand Up @@ -521,14 +559,21 @@ mod tests {
#[test]
fn test_general() {
let mut ini = Ini::new();
ini.parse_str("[GENERAL]\ndebug = true\nport = 1234\ncontrol-timeout=1.5\nseccomp = kill")
.unwrap();
ini.parse_str(
"[GENERAL]\ndebug = true\nport = 1234\ncontrol-timeout=1.5\n\
control-error-policy= basic-auth \nseccomp = kill",
)
.unwrap();
assert!(get_debug(&ini).unwrap());
assert_eq!(get_port(&ini).unwrap(), 1234);
assert_eq!(
get_control_timeout(&ini).unwrap(),
Duration::from_millis(1500)
);
assert_eq!(
get_control_error_policy(&ini).unwrap(),
ErrorPolicy::BasicAuth
);
assert_eq!(get_seccomp(&ini).unwrap(), Seccomp::Kill);
}

Expand Down
14 changes: 14 additions & 0 deletions letmeind/letmeind.conf
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ port = 5800
# Possible values: A positive number of seconds.
control-timeout = 5.0

# Control port error policy.
#
# If the policy is set to 'always', then error messages will always
# be transmitted to the connected client.
# If the policy is set to 'basic-auth', then error messages are suppressed
# unless the connected client has passed basic authentication.
# If the policy is set to 'full-auth', then error messages are suppressed
# unless the connected client has passed full authentication.
#
# Possible values: always, basic-auth, full-auth
# The recommended value is: basic-auth
# The default value is: always
control-error-policy = always

# Turn the Linux seccomp feature on.
#
# Possible values: off, log, kill
Expand Down
45 changes: 44 additions & 1 deletion letmeind/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,33 @@ use crate::{
server::ConnectionOps,
};
use anyhow::{self as ah, format_err as err};
use letmein_conf::{Config, Resource};
use letmein_conf::{Config, ErrorPolicy, Resource};
use letmein_proto::{Message, Operation, ResourceId, UserId};
use std::{net::SocketAddr, path::Path};
use tokio::time::timeout;

/// Protocol authentication state.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[allow(clippy::enum_variant_names)]
enum AuthState {
/// Not authenticated.
NotAuth,

/// Basic (not replay-safe) authentication passed.
BasicAuth,

/// Full challenge-response authentication passed.
ChallengeResponseAuth,
}

/// Implementation of the wire protocol message sequence.
pub struct Protocol<'a, C> {
conn: C,
conf: &'a Config,
rundir: &'a Path,
user_id: Option<UserId>,
resource_id: Option<ResourceId>,
auth_state: AuthState,
}

impl<'a, C: ConnectionOps> Protocol<'a, C> {
Expand All @@ -32,6 +48,7 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
rundir,
user_id: None,
resource_id: None,
auth_state: AuthState::NotAuth,
}
}

Expand Down Expand Up @@ -77,6 +94,24 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
}

async fn send_go_away(&mut self) -> ah::Result<()> {
// Check if we are allowed to send the error message.
match self.conf.control_error_policy() {
ErrorPolicy::Always => (),
ErrorPolicy::BasicAuth => {
if self.auth_state != AuthState::BasicAuth
&& self.auth_state != AuthState::ChallengeResponseAuth
{
return Ok(());
}
}
ErrorPolicy::FullAuth => {
if self.auth_state != AuthState::ChallengeResponseAuth {
return Ok(());
}
}
}

// Send the error message.
self.send_msg(&Message::new(
Operation::GoAway,
self.user_id.unwrap_or(u32::MAX.into()),
Expand All @@ -86,6 +121,11 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
}

pub async fn run(&mut self) -> ah::Result<()> {
self.user_id = None;
self.resource_id = None;
self.auth_state = AuthState::NotAuth;

// Receive the initial knock message.
let knock = self.recv_msg(Operation::Knock).await?;

let user_id = knock.user();
Expand All @@ -106,6 +146,7 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
let _ = self.send_go_away().await;
return Err(err!("Knock: Authentication failed"));
}
self.auth_state = AuthState::BasicAuth;

// Get the requested resource from the configuration.
let Some(resource) = self.conf.resource(resource_id) else {
Expand Down Expand Up @@ -153,6 +194,7 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
let _ = self.send_go_away().await;
return Err(err!("Response: Authentication failed"));
}
self.auth_state = AuthState::ChallengeResponseAuth;

// Reconfigure the firewall.
match resource {
Expand Down Expand Up @@ -180,6 +222,7 @@ impl<'a, C: ConnectionOps> Protocol<'a, C> {
};

// Send an open-port request to letmeinfwd.
assert_eq!(self.auth_state, AuthState::ChallengeResponseAuth);
if let Err(e) = fw.open_port(self.addr().ip(), port_type, *port).await {
let _ = self.send_go_away().await;
return Err(err!("letmeinfwd firewall open: {e}"));
Expand Down

0 comments on commit b34de0f

Please sign in to comment.