Check if public key is associated with the user

This commit is contained in:
Dreaded_X 2025-04-14 10:32:16 +02:00
parent 7f64ca1822
commit 1515ff236a
Signed by: Dreaded_X
GPG Key ID: 5A0CBFE3C3377FAA
7 changed files with 192 additions and 16 deletions

73
Cargo.lock generated
View File

@ -150,6 +150,17 @@ dependencies = [
"password-hash", "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]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -1559,6 +1570,40 @@ dependencies = [
"spin", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.171" version = "0.2.171"
@ -1647,6 +1692,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.4" version = "0.7.4"
@ -1706,6 +1757,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -3077,6 +3138,17 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.14" version = "0.7.14"
@ -3221,6 +3293,7 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-util", "hyper-util",
"ldap3",
"rand 0.8.5", "rand 0.8.5",
"ratatui", "ratatui",
"reqwest", "reqwest",

View File

@ -15,6 +15,7 @@ futures = "0.3.31"
http-body-util = { version = "0.1.3", features = ["full"] } http-body-util = { version = "0.1.3", features = ["full"] }
hyper = { version = "1.6.0", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] }
hyper-util = { version = "0.1.11", features = ["full"] } hyper-util = { version = "0.1.11", features = ["full"] }
ldap3 = "0.11.5"
rand = "0.8.5" rand = "0.8.5"
ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] } ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] }
reqwest = { version = "0.12.15", features = ["rustls-tls"] } reqwest = { version = "0.12.15", features = ["rustls-tls"] }

View File

@ -4,19 +4,33 @@ use clap::Parser as _;
use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend}; use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend};
use russh::{ use russh::{
ChannelId, ChannelId,
keys::ssh_key::PublicKey,
server::{Auth, Msg, Session}, server::{Auth, Msg, Session},
}; };
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use crate::{ use crate::{
cli, Ldap, cli,
input::Input, input::Input,
io::TerminalHandle, io::TerminalHandle,
ldap::LdapError,
tui::Renderer, tui::Renderer,
tunnel::{Tunnel, TunnelAccess, Tunnels}, 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 { pub struct Handler {
ldap: Ldap,
all_tunnels: Tunnels, all_tunnels: Tunnels,
tunnels: Vec<Tunnel>, tunnels: Vec<Tunnel>,
@ -31,8 +45,9 @@ pub struct Handler {
} }
impl Handler { impl Handler {
pub fn new(all_tunnels: Tunnels) -> Self { pub fn new(ldap: Ldap, all_tunnels: Tunnels) -> Self {
Self { Self {
ldap,
all_tunnels, all_tunnels,
tunnels: Default::default(), tunnels: Default::default(),
user: None, user: None,
@ -237,7 +252,7 @@ impl Handler {
} }
impl russh::server::Handler for Handler { impl russh::server::Handler for Handler {
type Error = russh::Error; type Error = HandlerError;
async fn channel_open_session( async fn channel_open_session(
&mut self, &mut self,
@ -252,14 +267,21 @@ impl russh::server::Handler for Handler {
async fn auth_publickey( async fn auth_publickey(
&mut self, &mut self,
user: &str, user: &str,
_public_key: &russh::keys::ssh_key::PublicKey, public_key: &PublicKey,
) -> Result<Auth, Self::Error> { ) -> Result<Auth, Self::Error> {
debug!("Login from {user}"); debug!("Login from {user}");
trace!("{public_key:?}");
self.user = Some(user.into()); self.user = Some(user.into());
// TODO: Get ssh keys associated with user from ldap for key in self.ldap.get_ssh_keys(user).await? {
Ok(Auth::Accept) trace!("{key:?}");
if key.key_data() == public_key.key_data() {
return Ok(Auth::Accept);
}
}
Ok(Auth::reject())
} }
async fn data( 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( async fn tcpip_forward(
@ -334,7 +356,7 @@ impl russh::server::Handler for Handler {
trace!(address, port, "tcpip_forward"); trace!(address, port, "tcpip_forward");
let Some(user) = self.user.clone() else { let Some(user) = self.user.clone() else {
return Err(russh::Error::Inconsistent); return Err(russh::Error::Inconsistent.into());
}; };
let tunnel = self let tunnel = self

74
src/ldap.rs Normal file
View File

@ -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<Ldap, LdapError> {
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<str>,
) -> Result<Vec<PublicKey>, 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::<Result<Vec<_>, _>>()
.map_err(russh::Error::from)?)
}
}

View File

@ -7,9 +7,11 @@ mod handler;
mod helper; mod helper;
mod input; mod input;
mod io; mod io;
mod ldap;
mod server; mod server;
mod tui; mod tui;
mod tunnel; mod tunnel;
pub use ldap::Ldap;
pub use server::Server; pub use server::Server;
pub use tunnel::{Tunnel, Tunnels}; pub use tunnel::{Tunnel, Tunnels};

View File

@ -8,7 +8,7 @@ use rand::rngs::OsRng;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::{error, info}; use tracing::{error, info};
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt};
use tunnel_rs::{Server, Tunnels}; use tunnel_rs::{Ldap, Server, Tunnels};
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<()> { async fn main() -> color_eyre::Result<()> {
@ -32,8 +32,10 @@ async fn main() -> color_eyre::Result<()> {
let authz_address = std::env::var("AUTHZ_ENDPOINT") let authz_address = std::env::var("AUTHZ_ENDPOINT")
.unwrap_or("http://localhost:9091/api/authz/forward-auth".into()); .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 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)); let addr = SocketAddr::from(([0, 0, 0, 0], 2222));
tokio::spawn(async move { ssh.run(key, addr).await }); tokio::spawn(async move { ssh.run(key, addr).await });
info!("SSH is available on {addr}"); info!("SSH is available on {addr}");

View File

@ -1,18 +1,19 @@
use std::{net::SocketAddr, sync::Arc, time::Duration}; 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 tokio::net::ToSocketAddrs;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::{handler::Handler, tunnel::Tunnels}; use crate::{Ldap, handler::Handler, tunnel::Tunnels};
pub struct Server { pub struct Server {
ldap: Ldap,
tunnels: Tunnels, tunnels: Tunnels,
} }
impl Server { impl Server {
pub fn new(tunnels: Tunnels) -> Self { pub fn new(ldap: Ldap, tunnels: Tunnels) -> Self {
Server { tunnels } Server { ldap, tunnels }
} }
pub fn tunnels(&self) -> Tunnels { pub fn tunnels(&self) -> Tunnels {
@ -26,13 +27,14 @@ impl Server {
) -> impl Future<Output = Result<(), std::io::Error>> + Send { ) -> impl Future<Output = Result<(), std::io::Error>> + Send {
let config = russh::server::Config { let config = russh::server::Config {
inactivity_timeout: Some(Duration::from_secs(3600)), 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)), auth_rejection_time_initial: Some(Duration::from_secs(0)),
keys: vec![key], keys: vec![key],
preferred: russh::Preferred { preferred: russh::Preferred {
..Default::default() ..Default::default()
}, },
nodelay: true, nodelay: true,
methods: [MethodKind::PublicKey].as_slice().into(),
..Default::default() ..Default::default()
}; };
let config = Arc::new(config); let config = Arc::new(config);
@ -47,7 +49,7 @@ impl russh::server::Server for Server {
type Handler = Handler; type Handler = Handler;
fn new_client(&mut self, _peer_addr: Option<SocketAddr>) -> Self::Handler { fn new_client(&mut self, _peer_addr: Option<SocketAddr>) -> Self::Handler {
Handler::new(self.tunnels.clone()) Handler::new(self.ldap.clone(), self.tunnels.clone())
} }
fn handle_session_error(&mut self, error: <Self::Handler as russh::server::Handler>::Error) { fn handle_session_error(&mut self, error: <Self::Handler as russh::server::Handler>::Error) {