1
1
use anyhow:: { Context , Result } ;
2
2
use bootc_utils:: CommandRunExt ;
3
+ use bootc_utils:: PathQuotedDisplay ;
3
4
use rustix:: fs:: Uid ;
4
5
use rustix:: process:: geteuid;
5
6
use rustix:: process:: getuid;
6
7
use rustix:: thread:: set_thread_res_uid;
7
8
use serde_json:: Value ;
9
+ use std:: collections:: BTreeMap ;
8
10
use std:: collections:: BTreeSet ;
9
11
use std:: fmt:: Display ;
10
12
use std:: fmt:: Formatter ;
13
+ use std:: os:: unix:: process:: CommandExt ;
11
14
use std:: process:: Command ;
12
15
use uzers:: os:: unix:: UserExt ;
13
16
@@ -82,7 +85,6 @@ impl Drop for UidChange {
82
85
pub ( crate ) struct UserKeys {
83
86
pub ( crate ) user : String ,
84
87
pub ( crate ) authorized_keys : String ,
85
- pub ( crate ) authorized_keys_path : String ,
86
88
}
87
89
88
90
impl UserKeys {
@@ -102,61 +104,135 @@ impl Display for UserKeys {
102
104
}
103
105
}
104
106
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
+
105
185
pub ( crate ) fn get_all_users_keys ( ) -> Result < Vec < UserKeys > > {
106
186
let loginctl_user_names = loginctl_users ( ) . context ( "enumerate users" ) ?;
107
187
108
188
let mut all_users_authorized_keys = Vec :: new ( ) ;
109
189
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
+
110
199
for user_name in loginctl_user_names {
111
200
let user_info = uzers:: get_user_by_name ( user_name. as_str ( ) )
112
201
. context ( format ! ( "user {} not found" , user_name) ) ?;
113
202
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 ( ) ) ;
123
207
}
124
208
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
+
125
217
let user_name = user_info
126
218
. name ( )
127
219
. to_str ( )
128
220
. context ( "user name is not valid utf-8" ) ?;
129
221
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
-
143
222
if user_authorized_keys. trim ( ) . is_empty ( ) {
144
223
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
147
226
) ;
148
227
continue ;
149
228
}
150
229
151
230
let user_keys = UserKeys {
152
231
user : user_name. to_string ( ) ,
153
232
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 ( ) ,
158
233
} ;
159
234
235
+ tracing:: trace!( "Found user keys: {:?}" , user_keys) ;
160
236
tracing:: debug!(
161
237
"Found user {} with {} SSH authorized_keys" ,
162
238
user_keys. user,
0 commit comments