From 1515ff236ae119ffd7141fa990317d1724323885 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Mon, 14 Apr 2025 10:32:16 +0200 Subject: [PATCH] Check if public key is associated with the user --- Cargo.lock | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/handler.rs | 38 ++++++++++++++++++++------ src/ldap.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ src/main.rs | 6 ++-- src/server.rs | 14 ++++++---- 7 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 src/ldap.rs diff --git a/Cargo.lock b/Cargo.lock index 91edff1..4d1c694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,17 @@ dependencies = [ "password-hash", ] +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1559,6 +1570,40 @@ dependencies = [ "spin", ] +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "native-tls", + "nom", + "percent-encoding", + "thiserror 1.0.69", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "libc" version = "0.2.171" @@ -1647,6 +1692,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1706,6 +1757,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3077,6 +3138,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.14" @@ -3221,6 +3293,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "ldap3", "rand 0.8.5", "ratatui", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index d98c99d..9be9e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ futures = "0.3.31" http-body-util = { version = "0.1.3", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] } hyper-util = { version = "0.1.11", features = ["full"] } +ldap3 = "0.11.5" rand = "0.8.5" ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] } reqwest = { version = "0.12.15", features = ["rustls-tls"] } diff --git a/src/handler.rs b/src/handler.rs index 2b8dc40..03b8d2f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -4,19 +4,33 @@ use clap::Parser as _; use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend}; use russh::{ ChannelId, + keys::ssh_key::PublicKey, server::{Auth, Msg, Session}, }; use tracing::{debug, trace, warn}; use crate::{ - cli, + Ldap, cli, input::Input, io::TerminalHandle, + ldap::LdapError, tui::Renderer, tunnel::{Tunnel, TunnelAccess, Tunnels}, }; +#[derive(Debug, thiserror::Error)] +pub enum HandlerError { + #[error(transparent)] + Russh(#[from] russh::Error), + #[error(transparent)] + Ldap(#[from] LdapError), + #[error(transparent)] + IO(#[from] std::io::Error), +} + pub struct Handler { + ldap: Ldap, + all_tunnels: Tunnels, tunnels: Vec, @@ -31,8 +45,9 @@ pub struct Handler { } impl Handler { - pub fn new(all_tunnels: Tunnels) -> Self { + pub fn new(ldap: Ldap, all_tunnels: Tunnels) -> Self { Self { + ldap, all_tunnels, tunnels: Default::default(), user: None, @@ -237,7 +252,7 @@ impl Handler { } impl russh::server::Handler for Handler { - type Error = russh::Error; + type Error = HandlerError; async fn channel_open_session( &mut self, @@ -252,14 +267,21 @@ impl russh::server::Handler for Handler { async fn auth_publickey( &mut self, user: &str, - _public_key: &russh::keys::ssh_key::PublicKey, + public_key: &PublicKey, ) -> Result { debug!("Login from {user}"); + trace!("{public_key:?}"); self.user = Some(user.into()); - // TODO: Get ssh keys associated with user from ldap - Ok(Auth::Accept) + for key in self.ldap.get_ssh_keys(user).await? { + trace!("{key:?}"); + if key.key_data() == public_key.key_data() { + return Ok(Auth::Accept); + } + } + + Ok(Auth::reject()) } async fn data( @@ -322,7 +344,7 @@ impl russh::server::Handler for Handler { } } - session.channel_success(channel) + Ok(session.channel_success(channel)?) } async fn tcpip_forward( @@ -334,7 +356,7 @@ impl russh::server::Handler for Handler { trace!(address, port, "tcpip_forward"); let Some(user) = self.user.clone() else { - return Err(russh::Error::Inconsistent); + return Err(russh::Error::Inconsistent.into()); }; let tunnel = self diff --git a/src/ldap.rs b/src/ldap.rs new file mode 100644 index 0000000..67b2f51 --- /dev/null +++ b/src/ldap.rs @@ -0,0 +1,74 @@ +use ldap3::{LdapConnAsync, SearchEntry}; +use russh::keys::PublicKey; + +#[derive(Debug, Clone)] +pub struct Ldap { + base: String, + ldap: ldap3::Ldap, +} + +#[derive(Debug, thiserror::Error)] +pub enum LdapError { + #[error(transparent)] + Ldap(#[from] ldap3::LdapError), + #[error("Key error: {0}")] + FailedToParseKey(#[from] russh::Error), + #[error("Mising environment variable: {0}")] + MissingEnvironmentVariable(&'static str), + #[error("Mising environment variable: {0}")] + CouldNotReadPasswordFile(#[from] std::io::Error), +} + +impl Ldap { + pub async fn start_from_env() -> Result { + let address = std::env::var("LDAP_ADDRESS") + .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_ADDRESS"))?; + let base = std::env::var("LDAP_BASE") + .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BASE"))?; + let bind_dn = std::env::var("LDAP_BIND_DN") + .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BIND_DN"))?; + + let password = std::env::var("LDAP_PASSWORD_FILE").map_or_else( + |_| { + std::env::var("LDAP_PASSWORD") + .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_PASSWORD")) + }, + |path| std::fs::read_to_string(path).map_err(|err| err.into()), + )?; + + let (conn, mut ldap) = LdapConnAsync::new(&address).await?; + ldap3::drive!(conn); + + ldap.simple_bind(&bind_dn, &password).await?.success()?; + + Ok(Self { base, ldap }) + } + + pub async fn get_ssh_keys( + &mut self, + user: impl AsRef, + ) -> Result, LdapError> { + Ok(self + .ldap + .search( + &self.base, + ldap3::Scope::Subtree, + // TODO: Make this not hardcoded + &format!("(uid={})", user.as_ref()), + vec!["sshkeys"], + ) + .await? + .success()? + .0 + .into_iter() + .map(SearchEntry::construct) + .flat_map(|entry| { + entry + .attrs + .into_values() + .flat_map(|keys| keys.into_iter().map(|key| PublicKey::from_openssh(&key))) + }) + .collect::, _>>() + .map_err(russh::Error::from)?) + } +} diff --git a/src/lib.rs b/src/lib.rs index b1b5753..1ec86c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,9 +7,11 @@ mod handler; mod helper; mod input; mod io; +mod ldap; mod server; mod tui; mod tunnel; +pub use ldap::Ldap; pub use server::Server; pub use tunnel::{Tunnel, Tunnels}; diff --git a/src/main.rs b/src/main.rs index b77ab57..eabaf82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use rand::rngs::OsRng; use tokio::net::TcpListener; use tracing::{error, info}; use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt}; -use tunnel_rs::{Server, Tunnels}; +use tunnel_rs::{Ldap, Server, Tunnels}; #[tokio::main] async fn main() -> color_eyre::Result<()> { @@ -32,8 +32,10 @@ async fn main() -> color_eyre::Result<()> { let authz_address = std::env::var("AUTHZ_ENDPOINT") .unwrap_or("http://localhost:9091/api/authz/forward-auth".into()); + let ldap = Ldap::start_from_env().await?; + let tunnels = Tunnels::new(domain, authz_address); - let mut ssh = Server::new(tunnels.clone()); + let mut ssh = Server::new(ldap, tunnels.clone()); let addr = SocketAddr::from(([0, 0, 0, 0], 2222)); tokio::spawn(async move { ssh.run(key, addr).await }); info!("SSH is available on {addr}"); diff --git a/src/server.rs b/src/server.rs index 0201dd9..eee3c8f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,18 +1,19 @@ use std::{net::SocketAddr, sync::Arc, time::Duration}; -use russh::{keys::PrivateKey, server::Server as _}; +use russh::{MethodKind, keys::PrivateKey, server::Server as _}; use tokio::net::ToSocketAddrs; use tracing::{debug, warn}; -use crate::{handler::Handler, tunnel::Tunnels}; +use crate::{Ldap, handler::Handler, tunnel::Tunnels}; pub struct Server { + ldap: Ldap, tunnels: Tunnels, } impl Server { - pub fn new(tunnels: Tunnels) -> Self { - Server { tunnels } + pub fn new(ldap: Ldap, tunnels: Tunnels) -> Self { + Server { ldap, tunnels } } pub fn tunnels(&self) -> Tunnels { @@ -26,13 +27,14 @@ impl Server { ) -> impl Future> + Send { let config = russh::server::Config { inactivity_timeout: Some(Duration::from_secs(3600)), - auth_rejection_time: Duration::from_secs(3), + auth_rejection_time: Duration::from_secs(1), auth_rejection_time_initial: Some(Duration::from_secs(0)), keys: vec![key], preferred: russh::Preferred { ..Default::default() }, nodelay: true, + methods: [MethodKind::PublicKey].as_slice().into(), ..Default::default() }; let config = Arc::new(config); @@ -47,7 +49,7 @@ impl russh::server::Server for Server { type Handler = Handler; fn new_client(&mut self, _peer_addr: Option) -> Self::Handler { - Handler::new(self.tunnels.clone()) + Handler::new(self.ldap.clone(), self.tunnels.clone()) } fn handle_session_error(&mut self, error: ::Error) {