2024-01-15 16:18:46 +00:00
use structopt ::StructOpt ;
use garage_util ::config ::Config ;
use garage_util ::error ::Error ;
/// Structure for secret values or paths that are passed as CLI arguments or environment
/// variables, instead of in the config file.
#[ derive(StructOpt, Debug, Default, Clone) ]
pub struct Secrets {
/// Skip permission check on files containing secrets
#[ cfg(unix) ]
#[ structopt(
long = " allow-world-readable-secrets " ,
env = " GARAGE_ALLOW_WORLD_READABLE_SECRETS "
) ]
pub allow_world_readable_secrets : Option < bool > ,
/// RPC secret network key, used to replace rpc_secret in config.toml when running the
/// daemon or doing admin operations
#[ structopt(short = " s " , long = " rpc-secret " , env = " GARAGE_RPC_SECRET " ) ]
pub rpc_secret : Option < String > ,
/// RPC secret network key, used to replace rpc_secret in config.toml and rpc-secret
/// when running the daemon or doing admin operations
#[ structopt(long = " rpc-secret-file " , env = " GARAGE_RPC_SECRET_FILE " ) ]
pub rpc_secret_file : Option < String > ,
/// Admin API authentication token, replaces admin.admin_token in config.toml when
/// running the Garage daemon
#[ structopt(long = " admin-token " , env = " GARAGE_ADMIN_TOKEN " ) ]
pub admin_token : Option < String > ,
/// Admin API authentication token file path, replaces admin.admin_token in config.toml
/// and admin-token when running the Garage daemon
#[ structopt(long = " admin-token-file " , env = " GARAGE_ADMIN_TOKEN_FILE " ) ]
pub admin_token_file : Option < String > ,
/// Metrics API authentication token, replaces admin.metrics_token in config.toml when
/// running the Garage daemon
#[ structopt(long = " metrics-token " , env = " GARAGE_METRICS_TOKEN " ) ]
pub metrics_token : Option < String > ,
/// Metrics API authentication token file path, replaces admin.metrics_token in config.toml
/// and metrics-token when running the Garage daemon
#[ structopt(long = " metrics-token-file " , env = " GARAGE_METRICS_TOKEN_FILE " ) ]
pub metrics_token_file : Option < String > ,
}
/// Single function to fill all secrets in the Config struct from their correct source (value
/// from config or CLI param or env variable or read from a file specified in config or CLI
/// param or env variable)
pub fn fill_secrets ( mut config : Config , secrets : Secrets ) -> Result < Config , Error > {
let allow_world_readable = secrets
. allow_world_readable_secrets
. unwrap_or ( config . allow_world_readable_secrets ) ;
fill_secret (
& mut config . rpc_secret ,
& config . rpc_secret_file ,
& secrets . rpc_secret ,
& secrets . rpc_secret_file ,
" rpc_secret " ,
allow_world_readable ,
) ? ;
fill_secret (
& mut config . admin . admin_token ,
& config . admin . admin_token_file ,
& secrets . admin_token ,
& secrets . admin_token_file ,
" admin.admin_token " ,
allow_world_readable ,
) ? ;
fill_secret (
& mut config . admin . metrics_token ,
& config . admin . metrics_token_file ,
& secrets . metrics_token ,
& secrets . metrics_token_file ,
" admin.metrics_token " ,
allow_world_readable ,
) ? ;
Ok ( config )
}
fn fill_secret (
config_secret : & mut Option < String > ,
config_secret_file : & Option < String > ,
cli_secret : & Option < String > ,
cli_secret_file : & Option < String > ,
name : & 'static str ,
allow_world_readable : bool ,
) -> Result < ( ) , Error > {
let cli_value = match ( & cli_secret , & cli_secret_file ) {
( Some ( _ ) , Some ( _ ) ) = > {
return Err ( format! ( " only one of ` {} ` and ` {} _file` can be set " , name , name ) . into ( ) ) ;
}
( Some ( secret ) , None ) = > Some ( secret . to_string ( ) ) ,
( None , Some ( file ) ) = > Some ( read_secret_file ( file , allow_world_readable ) ? ) ,
( None , None ) = > None ,
} ;
if let Some ( val ) = cli_value {
if config_secret . is_some ( ) | | config_secret_file . is_some ( ) {
debug! ( " Overriding secret `{}` using value specified using CLI argument or environnement variable. " , name ) ;
}
* config_secret = Some ( val ) ;
} else if let Some ( file_path ) = & config_secret_file {
if config_secret . is_some ( ) {
return Err ( format! ( " only one of ` {} ` and ` {} _file` can be set " , name , name ) . into ( ) ) ;
}
* config_secret = Some ( read_secret_file ( file_path , allow_world_readable ) ? ) ;
}
Ok ( ( ) )
}
fn read_secret_file ( file_path : & String , allow_world_readable : bool ) -> Result < String , Error > {
if ! allow_world_readable {
#[ cfg(unix) ]
{
use std ::os ::unix ::fs ::MetadataExt ;
let metadata = std ::fs ::metadata ( file_path ) ? ;
if metadata . mode ( ) & 0o077 ! = 0 {
return Err ( format! ( " File {} is world-readable! (mode: 0 {:o} , expected 0600) \n Refusing to start until this is fixed, or environment variable GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true. " , file_path , metadata . mode ( ) ) . into ( ) ) ;
}
}
}
let secret_buf = std ::fs ::read_to_string ( file_path ) ? ;
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
// also editors sometimes add a trailing newline
Ok ( String ::from ( secret_buf . trim_end ( ) ) )
}
#[ cfg(test) ]
mod tests {
use std ::fs ::File ;
use std ::io ::Write ;
use garage_util ::config ::read_config ;
use garage_util ::error ::Error ;
use super ::* ;
#[ test ]
fn test_rpc_secret_file_works ( ) -> Result < ( ) , Error > {
let path_secret = mktemp ::Temp ::new_file ( ) ? ;
let mut file_secret = File ::create ( path_secret . as_path ( ) ) ? ;
writeln! ( file_secret , " foo " ) ? ;
drop ( file_secret ) ;
let path_config = mktemp ::Temp ::new_file ( ) ? ;
let mut file_config = File ::create ( path_config . as_path ( ) ) ? ;
let path_secret_path = path_secret . as_path ( ) ;
writeln! (
file_config ,
r #"
metadata_dir = " /tmp/garage/meta "
data_dir = " /tmp/garage/data "
replication_mode = " 3 "
rpc_bind_addr = " [::]:3901 "
rpc_secret_file = " {} "
[ s3_api ]
s3_region = " garage "
api_bind_addr = " [::]:3900 "
" #,
path_secret_path . display ( )
) ? ;
drop ( file_config ) ;
// Second configuration file, same as previous one
// except it allows world-readable secrets.
let path_config_allow_world_readable = mktemp ::Temp ::new_file ( ) ? ;
let mut file_config_allow_world_readable =
File ::create ( path_config_allow_world_readable . as_path ( ) ) ? ;
writeln! (
file_config_allow_world_readable ,
r #"
metadata_dir = " /tmp/garage/meta "
data_dir = " /tmp/garage/data "
replication_mode = " 3 "
rpc_bind_addr = " [::]:3901 "
rpc_secret_file = " {} "
allow_world_readable_secrets = true
[ s3_api ]
s3_region = " garage "
api_bind_addr = " [::]:3900 "
" #,
path_secret_path . display ( )
) ? ;
drop ( file_config_allow_world_readable ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
let config = fill_secrets ( config , Secrets ::default ( ) ) ? ;
assert_eq! ( " foo " , config . rpc_secret . unwrap ( ) ) ;
2024-01-15 16:30:30 +00:00
// ---- Check non world-readable secrets config ----
2024-01-15 16:18:46 +00:00
#[ cfg(unix) ]
{
let secrets_allow_world_readable = Secrets {
allow_world_readable_secrets : Some ( true ) ,
.. Default ::default ( )
} ;
let secrets_no_allow_world_readable = Secrets {
allow_world_readable_secrets : Some ( false ) ,
.. Default ::default ( )
} ;
use std ::os ::unix ::fs ::PermissionsExt ;
let metadata = std ::fs ::metadata ( & path_secret_path ) ? ;
let mut perm = metadata . permissions ( ) ;
perm . set_mode ( 0o660 ) ;
std ::fs ::set_permissions ( & path_secret_path , perm ) ? ;
// Config file that just specifies the path
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , Secrets ::default ( ) ) . is_err ( ) ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , secrets_allow_world_readable . clone ( ) ) . is_ok ( ) ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , secrets_no_allow_world_readable . clone ( ) ) . is_err ( ) ) ;
// Config file that also specifies to allow world_readable_secrets
let config = read_config ( path_config_allow_world_readable . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , Secrets ::default ( ) ) . is_ok ( ) ) ;
let config = read_config ( path_config_allow_world_readable . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , secrets_allow_world_readable ) . is_ok ( ) ) ;
let config = read_config ( path_config_allow_world_readable . to_path_buf ( ) ) ? ;
assert! ( fill_secrets ( config , secrets_no_allow_world_readable ) . is_err ( ) ) ;
}
2024-01-15 16:30:30 +00:00
// ---- Check alternative secrets specified on CLI ----
let path_secret2 = mktemp ::Temp ::new_file ( ) ? ;
let mut file_secret2 = File ::create ( path_secret2 . as_path ( ) ) ? ;
writeln! ( file_secret2 , " bar " ) ? ;
drop ( file_secret2 ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
let config = fill_secrets (
config ,
Secrets {
rpc_secret : Some ( " baz " . into ( ) ) ,
.. Default ::default ( )
} ,
) ? ;
assert_eq! ( config . rpc_secret . as_deref ( ) , Some ( " baz " ) ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
let config = fill_secrets (
config ,
Secrets {
rpc_secret_file : Some ( path_secret2 . display ( ) . to_string ( ) ) ,
.. Default ::default ( )
} ,
) ? ;
assert_eq! ( config . rpc_secret . as_deref ( ) , Some ( " bar " ) ) ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
assert! ( fill_secrets (
config ,
Secrets {
rpc_secret : Some ( " baz " . into ( ) ) ,
rpc_secret_file : Some ( path_secret2 . display ( ) . to_string ( ) ) ,
.. Default ::default ( )
}
)
. is_err ( ) ) ;
2024-01-15 16:18:46 +00:00
drop ( path_secret ) ;
2024-01-15 16:30:30 +00:00
drop ( path_secret2 ) ;
2024-01-15 16:18:46 +00:00
drop ( path_config ) ;
drop ( path_config_allow_world_readable ) ;
Ok ( ( ) )
}
#[ test ]
fn test_rcp_secret_and_rpc_secret_file_cannot_be_set_both ( ) -> Result < ( ) , Error > {
let path_config = mktemp ::Temp ::new_file ( ) ? ;
let mut file_config = File ::create ( path_config . as_path ( ) ) ? ;
writeln! (
file_config ,
r #"
metadata_dir = " /tmp/garage/meta "
data_dir = " /tmp/garage/data "
replication_mode = " 3 "
rpc_bind_addr = " [::]:3901 "
rpc_secret = " dummy "
rpc_secret_file = " dummy "
[ s3_api ]
s3_region = " garage "
api_bind_addr = " [::]:3900 "
" #
) ? ;
let config = read_config ( path_config . to_path_buf ( ) ) ? ;
assert_eq! (
" only one of `rpc_secret` and `rpc_secret_file` can be set " ,
fill_secrets ( config , Secrets ::default ( ) )
. unwrap_err ( )
. to_string ( )
) ;
drop ( path_config ) ;
drop ( file_config ) ;
Ok ( ( ) )
}
}