Separate SSH key probe from authentication

This commit is contained in:
ZhenYi 2026-05-14 21:44:55 +08:00
parent b8bd0ec545
commit a3ecf0c88b
3 changed files with 75 additions and 16 deletions

View File

@ -14,6 +14,12 @@ pub struct SshAuthService {
db: AppDatabase, db: AppDatabase,
} }
pub struct SshKeyUser {
pub user: user::Model,
pub key_id: i64,
pub key_title: String,
}
impl SshAuthService { impl SshAuthService {
pub fn new(db: AppDatabase) -> Self { pub fn new(db: AppDatabase) -> Self {
Self { db } Self { db }
@ -97,7 +103,7 @@ impl SshAuthService {
pub async fn find_user_by_public_key( pub async fn find_user_by_public_key(
&self, &self,
public_key_str: &str, public_key_str: &str,
) -> Result<Option<user::Model>, DbErr> { ) -> Result<Option<SshKeyUser>, DbErr> {
let fingerprint = match self.generate_fingerprint_from_public_key(public_key_str) { let fingerprint = match self.generate_fingerprint_from_public_key(public_key_str) {
Ok(fp) => fp, Ok(fp) => fp,
Err(e) => { Err(e) => {
@ -144,16 +150,20 @@ impl SshAuthService {
.one(self.db.reader()) .one(self.db.reader())
.await?; .await?;
if let Some(ref user) = user_model { if let Some(user) = user_model {
tracing::info!( tracing::info!(
"user authenticated via SSH key user={} key={}", "SSH key matched user={} key={}",
user.username, user.username,
ssh_key.title ssh_key.title
); );
self.update_key_last_used_async(ssh_key.id); return Ok(Some(SshKeyUser {
user,
key_id: ssh_key.id,
key_title: ssh_key.title,
}));
} }
Ok(user_model) Ok(None)
} }
fn is_key_expired(&self, ssh_key: &user_ssh_key::Model) -> bool { fn is_key_expired(&self, ssh_key: &user_ssh_key::Model) -> bool {
@ -165,7 +175,7 @@ impl SshAuthService {
} }
} }
fn update_key_last_used_async(&self, key_id: i64) { pub fn update_key_last_used_async(&self, key_id: i64) {
let db_clone = self.db.clone(); let db_clone = self.db.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = Self::update_key_last_used_sync(db_clone, key_id).await { if let Err(e) = Self::update_key_last_used_sync(db_clone, key_id).await {

View File

@ -163,7 +163,52 @@ impl russh::server::Handler for SSHandle {
user: &str, user: &str,
public_key: &PublicKey, public_key: &PublicKey,
) -> Result<Auth, Self::Error> { ) -> Result<Auth, Self::Error> {
self.auth_publickey(user, public_key).await let client_info = self
.client_addr
.map(|addr| format!("{}", addr))
.unwrap_or_else(|| "unknown".to_string());
if user != "git" {
tracing::warn!(
"auth_publickey_offer_rejected_invalid_username user={} client={}",
user,
client_info
);
return Err(russh::Error::NotAuthenticated);
}
let public_key_str = public_key.to_string();
if public_key_str.len() < 32 {
tracing::warn!(
"auth_publickey_offer_rejected_invalid_key_length key_length={}",
public_key_str.len()
);
return Err(russh::Error::NotAuthenticated);
}
tracing::info!("auth_publickey_offer client={}", client_info);
match self.auth.find_user_by_public_key(&public_key_str).await {
Ok(Some(key_user)) => {
tracing::info!(
"auth_publickey_offer_accepted user={} key={} client={}",
key_user.user.username,
key_user.key_title,
client_info
);
Ok(Auth::Accept)
}
Ok(None) => {
tracing::warn!(
"auth_publickey_offer_rejected_key_not_found client={}",
client_info
);
Err(russh::Error::NotAuthenticated)
}
Err(e) => {
tracing::error!("auth_publickey_offer_error error={}", e);
Err(russh::Error::NotAuthenticated)
}
}
} }
async fn auth_publickey( async fn auth_publickey(
&mut self, &mut self,
@ -193,8 +238,8 @@ impl russh::server::Handler for SSHandle {
} }
tracing::info!("auth_publickey_attempt client={}", client_info); tracing::info!("auth_publickey_attempt client={}", client_info);
let user_model = match self.auth.find_user_by_public_key(&public_key_str).await { let key_user = match self.auth.find_user_by_public_key(&public_key_str).await {
Ok(Some(model)) => model, Ok(Some(key_user)) => key_user,
Ok(None) => { Ok(None) => {
tracing::warn!("auth_rejected_key_not_found client={}", client_info); tracing::warn!("auth_rejected_key_not_found client={}", client_info);
return Err(russh::Error::NotAuthenticated); return Err(russh::Error::NotAuthenticated);
@ -207,10 +252,11 @@ impl russh::server::Handler for SSHandle {
tracing::info!( tracing::info!(
"auth_publickey_success user={} client={}", "auth_publickey_success user={} client={}",
user_model.username, key_user.user.username,
client_info client_info
); );
self.operator = Some(user_model); self.auth.update_key_last_used_async(key_user.key_id);
self.operator = Some(key_user.user);
Ok(Auth::Accept) Ok(Auth::Accept)
} }
async fn auth_openssh_certificate( async fn auth_openssh_certificate(
@ -241,8 +287,8 @@ impl russh::server::Handler for SSHandle {
} }
tracing::info!("auth_publickey_attempt client={}", client_info); tracing::info!("auth_publickey_attempt client={}", client_info);
let user_model = match self.auth.find_user_by_public_key(&public_key_str).await { let key_user = match self.auth.find_user_by_public_key(&public_key_str).await {
Ok(Some(model)) => model, Ok(Some(key_user)) => key_user,
Ok(None) => { Ok(None) => {
tracing::warn!("auth_rejected_key_not_found client={}", client_info); tracing::warn!("auth_rejected_key_not_found client={}", client_info);
return Err(russh::Error::NotAuthenticated); return Err(russh::Error::NotAuthenticated);
@ -255,10 +301,11 @@ impl russh::server::Handler for SSHandle {
tracing::info!( tracing::info!(
"auth_publickey_success user={} client={}", "auth_publickey_success user={} client={}",
user_model.username, key_user.user.username,
client_info client_info
); );
self.operator = Some(user_model); self.auth.update_key_last_used_async(key_user.key_id);
self.operator = Some(key_user.user);
Ok(Auth::Accept) Ok(Auth::Accept)
} }
async fn authentication_banner(&mut self) -> Result<Option<String>, Self::Error> { async fn authentication_banner(&mut self) -> Result<Option<String>, Self::Error> {

View File

@ -82,7 +82,9 @@ impl russh::server::Server for SSHServer {
); );
if io_err.kind() == io::ErrorKind::UnexpectedEof { if io_err.kind() == io::ErrorKind::UnexpectedEof {
tracing::warn!("Client disconnected during handshake or before authentication"); tracing::warn!(
"SSH peer closed the connection before a clean disconnect was received"
);
} }
} }
_ => { _ => {