11use anyhow:: { Context , Result } ;
22use bootc_utils:: CommandRunExt ;
3+ use bootc_utils:: PathQuotedDisplay ;
34use rustix:: fs:: Uid ;
45use rustix:: process:: geteuid;
56use rustix:: process:: getuid;
67use rustix:: thread:: set_thread_res_uid;
78use serde_json:: Value ;
9+ use std:: collections:: BTreeMap ;
810use std:: collections:: BTreeSet ;
911use std:: fmt:: Display ;
1012use std:: fmt:: Formatter ;
13+ use std:: os:: unix:: process:: CommandExt ;
1114use std:: process:: Command ;
1215use uzers:: os:: unix:: UserExt ;
1316
@@ -82,7 +85,6 @@ impl Drop for UidChange {
8285pub ( crate ) struct UserKeys {
8386 pub ( crate ) user : String ,
8487 pub ( crate ) authorized_keys : String ,
85- pub ( crate ) authorized_keys_path : String ,
8688}
8789
8890impl UserKeys {
@@ -102,61 +104,135 @@ impl Display for UserKeys {
102104 }
103105}
104106
107+ #[ derive( Debug ) ]
108+ struct SshdConfig < ' a > {
109+ authorized_keys_files : Vec < & ' a str > ,
110+ authorized_keys_command : & ' a str ,
111+ authorized_keys_command_user : & ' a str ,
112+ }
113+
114+ impl < ' a > SshdConfig < ' a > {
115+ pub fn parse ( sshd_output : & ' a str ) -> Result < SshdConfig < ' a > > {
116+ let config = sshd_output
117+ . lines ( )
118+ . filter_map ( |line| line. split_once ( ' ' ) )
119+ . collect :: < BTreeMap < & str , & str > > ( ) ;
120+
121+ let authorized_keys_files: Vec < & str > = config
122+ . get ( "authorizedkeysfile" )
123+ . unwrap_or ( & "none" )
124+ . split_whitespace ( )
125+ . collect ( ) ;
126+ let authorized_keys_command = config. get ( "authorizedkeyscommand" ) . unwrap_or ( & "none" ) ;
127+ let authorized_keys_command_user =
128+ config. get ( "authorizedkeyscommanduser" ) . unwrap_or ( & "none" ) ;
129+
130+ Ok ( Self {
131+ authorized_keys_files,
132+ authorized_keys_command,
133+ authorized_keys_command_user,
134+ } )
135+ }
136+ }
137+
138+ fn get_keys_from_files ( user : & uzers:: User , keyfiles : & Vec < & str > ) -> Result < String > {
139+ let home_dir = user. home_dir ( ) ;
140+ let mut user_authorized_keys = String :: new ( ) ;
141+
142+ for keyfile in keyfiles {
143+ let user_authorized_keys_path = home_dir. join ( keyfile) ;
144+
145+ if !user_authorized_keys_path. exists ( ) {
146+ tracing:: debug!(
147+ "Skipping authorized key file {} for user {} because it doesn't exist" ,
148+ PathQuotedDisplay :: new( & user_authorized_keys_path) ,
149+ user. name( ) . to_string_lossy( )
150+ ) ;
151+ continue ;
152+ }
153+
154+ // Safety: The UID should be valid because we got it from uzers
155+ #[ allow( unsafe_code) ]
156+ let user_uid = unsafe { Uid :: from_raw ( user. uid ( ) ) } ;
157+
158+ // Change the effective uid for this scope, to avoid accidentally reading files we
159+ // shouldn't through symlinks
160+ let _uid_change = UidChange :: new ( user_uid) ?;
161+
162+ let key = std:: fs:: read_to_string ( & user_authorized_keys_path)
163+ . context ( "Failed to read user's authorized keys" ) ?;
164+ user_authorized_keys. push_str ( key. as_str ( ) ) ;
165+ user_authorized_keys. push ( '\n' ) ;
166+ }
167+
168+ Ok ( user_authorized_keys)
169+ }
170+
171+ fn get_keys_from_command ( command : & str , command_user : & str ) -> Result < String > {
172+ let user_config = uzers:: get_user_by_name ( command_user) . context ( format ! (
173+ "authorized_keys_command_user {} not found" ,
174+ command_user
175+ ) ) ?;
176+
177+ let mut cmd = Command :: new ( command) ;
178+ cmd. uid ( user_config. uid ( ) ) ;
179+ let output = cmd
180+ . run_get_string ( )
181+ . context ( format ! ( "running authorized_keys_command {}" , command) ) ?;
182+ Ok ( output)
183+ }
184+
105185pub ( crate ) fn get_all_users_keys ( ) -> Result < Vec < UserKeys > > {
106186 let loginctl_user_names = loginctl_users ( ) . context ( "enumerate users" ) ?;
107187
108188 let mut all_users_authorized_keys = Vec :: new ( ) ;
109189
190+ let sshd_output = Command :: new ( "sshd" )
191+ . arg ( "-T" )
192+ . run_get_string ( )
193+ . context ( "running sshd -T" ) ?;
194+ tracing:: trace!( "sshd output:\n {}" , sshd_output) ;
195+
196+ let sshd_config = SshdConfig :: parse ( sshd_output. as_str ( ) ) ?;
197+ tracing:: debug!( "parsed sshd config: {:?}" , sshd_config) ;
198+
110199 for user_name in loginctl_user_names {
111200 let user_info = uzers:: get_user_by_name ( user_name. as_str ( ) )
112201 . context ( format ! ( "user {} not found" , user_name) ) ?;
113202
114- let home_dir = user_info. home_dir ( ) ;
115- let user_authorized_keys_path = home_dir. join ( ".ssh/authorized_keys" ) ;
116-
117- if !user_authorized_keys_path. exists ( ) {
118- tracing:: debug!(
119- "Skipping user {} because it doesn't have an SSH authorized_keys file" ,
120- user_info. name( ) . to_string_lossy( )
121- ) ;
122- continue ;
203+ let mut user_authorized_keys = String :: new ( ) ;
204+ if !sshd_config. authorized_keys_files . is_empty ( ) {
205+ let keys = get_keys_from_files ( & user_info, & sshd_config. authorized_keys_files ) ?;
206+ user_authorized_keys. push_str ( keys. as_str ( ) ) ;
123207 }
124208
209+ if sshd_config. authorized_keys_command != "none" {
210+ let keys = get_keys_from_command (
211+ & sshd_config. authorized_keys_command ,
212+ & sshd_config. authorized_keys_command_user ,
213+ ) ?;
214+ user_authorized_keys. push_str ( keys. as_str ( ) ) ;
215+ } ;
216+
125217 let user_name = user_info
126218 . name ( )
127219 . to_str ( )
128220 . context ( "user name is not valid utf-8" ) ?;
129221
130- let user_authorized_keys = {
131- // Safety: The UID should be valid because we got it from uzers
132- #[ allow( unsafe_code) ]
133- let user_uid = unsafe { Uid :: from_raw ( user_info. uid ( ) ) } ;
134-
135- // Change the effective uid for this scope, to avoid accidentally reading files we
136- // shouldn't through symlinks
137- let _uid_change = UidChange :: new ( user_uid) ?;
138-
139- std:: fs:: read_to_string ( & user_authorized_keys_path)
140- . context ( "Failed to read user's authorized keys" ) ?
141- } ;
142-
143222 if user_authorized_keys. trim ( ) . is_empty ( ) {
144223 tracing:: debug!(
145- "Skipping user {} because it has an empty SSH authorized_keys file " ,
146- user_info . name ( ) . to_string_lossy ( )
224+ "Skipping user {} because it has no SSH authorized_keys" ,
225+ user_name
147226 ) ;
148227 continue ;
149228 }
150229
151230 let user_keys = UserKeys {
152231 user : user_name. to_string ( ) ,
153232 authorized_keys : user_authorized_keys,
154- authorized_keys_path : user_authorized_keys_path
155- . to_str ( )
156- . context ( "user's authorized_keys path is not valid utf-8" ) ?
157- . to_string ( ) ,
158233 } ;
159234
235+ tracing:: trace!( "Found user keys: {:?}" , user_keys) ;
160236 tracing:: debug!(
161237 "Found user {} with {} SSH authorized_keys" ,
162238 user_keys. user,
0 commit comments