Compare commits

...

2 commits

Author SHA1 Message Date
514f83f90c
Better rwx checks 2023-07-28 02:04:51 -07:00
f138148581
Harden systemd unit 2023-07-28 02:04:29 -07:00
2 changed files with 51 additions and 12 deletions

View file

@ -57,7 +57,7 @@ pub struct Args {
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
pub enum Command { pub enum Command {
/// Fetch a reflected IP address and update A and AAAA entries in DNS. /// Fetch a reflected IP address and update A and AAAA entries in DNS.
Run, Run(Run),
/// List all A and AAAA entries in each zone in the config. /// List all A and AAAA entries in each zone in the config.
List(List), List(List),
} }
@ -107,6 +107,13 @@ impl Display for Color {
} }
} }
#[derive(Parser, Clone, Debug)]
pub struct Run {
/// The directory to store cache files.
#[clap(long)]
cache_dir: Option<PathBuf>,
}
fn main() -> ExitCode { fn main() -> ExitCode {
let runtime = Runtime::new().unwrap(); let runtime = Runtime::new().unwrap();
let result = runtime.block_on(real_main()); let result = runtime.block_on(real_main());
@ -149,12 +156,12 @@ async fn real_main() -> Result<()> {
let config = load_config(args.config_file).context("Failed to find a suitable config file")?; let config = load_config(args.config_file).context("Failed to find a suitable config file")?;
match args.cmd { match args.cmd {
Command::Run => handle_run(config).await, Command::Run(run) => handle_run(config, run).await,
Command::List(list) => handle_list(config, list).await, Command::List(list) => handle_list(config, list).await,
} }
} }
async fn handle_run(conf: Config) -> Result<()> { async fn handle_run(conf: Config, run: Run) -> Result<()> {
let ipv4 = if let Some(addr_to_req) = conf.ip_reflector.ipv4 { let ipv4 = if let Some(addr_to_req) = conf.ip_reflector.ipv4 {
let ip = get_ipv4(addr_to_req) let ip = get_ipv4(addr_to_req)
.await .await
@ -176,7 +183,7 @@ async fn handle_run(conf: Config) -> Result<()> {
None None
}; };
let ip_cache_path = ip_cache_path().context("while getting the ip cache path")?; let ip_cache_path = ip_cache_path(run.cache_dir).context("while getting the ip cache path")?;
let mut cache_file = load_ip_cache(&ip_cache_path).context("while loading the ip cache")?; let mut cache_file = load_ip_cache(&ip_cache_path).context("while loading the ip cache")?;
let mut rate_limit = time::interval(Duration::from_millis(250)); let mut rate_limit = time::interval(Duration::from_millis(250));
@ -293,8 +300,10 @@ fn load_ip_cache<P: AsRef<Path> + Debug>(path: P) -> Result<CacheFile> {
}) })
} }
fn ip_cache_path() -> Result<PathBuf> { #[instrument(level = "debug", ret)]
dirs::cache_dir() fn ip_cache_path(cache_dir: Option<PathBuf>) -> Result<PathBuf> {
cache_dir
.or(dirs::cache_dir())
.context("Failed to determine cache directory") .context("Failed to determine cache directory")
.map(|path| path.join("cloudflare-ddns.cache")) .map(|path| path.join("cloudflare-ddns.cache"))
} }
@ -519,13 +528,25 @@ fn load_config_from_path<P: AsRef<Path>>(path: P) -> Option<Config> {
// mode is a u32, but only the bottom 9 bits represent the // mode is a u32, but only the bottom 9 bits represent the
// permissions. Mask and keep the bits we care about. // permissions. Mask and keep the bits we care about.
let current_mode = metadata.permissions().mode() & 0o777; let current_mode = metadata.permissions().mode() & 0o777;
if current_mode != 0o600 { debug!(found = format!("{current_mode:o}"), "Metadata bits");
// Check if it's readable by others
if (current_mode & 0o077) > 0 {
warn!( warn!(
found = format!("{current_mode:o}"), found = format!("{current_mode:o}"),
expected = "600", expected = "600",
"File permissions too broad! Your GLOBAL Cloudflare API key is accessible to all users on the system!" "File permissions too broad! Your GLOBAL Cloudflare API key is accessible to all users on the system!"
); );
} }
// Check if executable bit is set
if (current_mode & 0o100) != 0 {
warn!(
found = format!("{current_mode:o}"),
expected = "600",
"Config file has executable bit set"
);
}
} }
Err(e) => { Err(e) => {
warn!("Failed to read metadata for file: {e}"); warn!("Failed to read metadata for file: {e}");

View file

@ -5,13 +5,16 @@ After=network-online.target
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/cloudflare-ddns run ExecStart=/usr/bin/cloudflare-ddns run --config-file "${CREDENTIALS_DIRECTORY}/cloudflare-ddns.toml" --cache-dir "${CACHE_DIRECTORY}"
# Please modify the path after the : to point to a custom config location if you'd like
LoadCredential=cloudflare-ddns.toml:/etc/cloudflare-ddns.toml
# Security Hardening
# Run `systemd-analyze security cloudflare-ddns` for recommendations
# Security
NoNewPrivileges=true NoNewPrivileges=true
ProtectSystem=strict
# Sandboxing config
ProtectSystem=true
PrivateTmp=true PrivateTmp=true
PrivateDevices=true PrivateDevices=true
ProtectHostname=true ProtectHostname=true
@ -25,6 +28,21 @@ LockPersonality=true
MemoryDenyWriteExecute=true MemoryDenyWriteExecute=true
RestrictRealtime=true RestrictRealtime=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
SystemCallFilter=~@resources
ProtectProc=invisible
ProcSubset=pid
RestrictAddressFamilies=AF_INET AF_INET6
UMask=066
DynamicUser=true
CacheDirectory=cloudflare-ddns
PrivateUsers=true
ProtectHome=true
# Refuse to execute any other binary
ExecPaths=/usr/bin/cloudflare-ddns
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target