From: Cameron Otsuka Date: Sun, 27 Jul 2025 03:56:31 +0000 (-0700) Subject: improve systemd service hardening, simplify and refactor logic, add rfc to readme X-Git-Url: https://git.otsuka.systems/?a=commitdiff_plain;h=refs%2Fheads%2Fmain;p=qotdd improve systemd service hardening, simplify and refactor logic, add rfc to readme --- diff --git a/Cargo.toml b/Cargo.toml index 9f9ac9c..c0ba64b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qotdd" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] diff --git a/README.md b/README.md index b601f64..6603aa2 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# qotdd \ No newline at end of file +# qotdd +[RFC 865](https://www.rfc-editor.org/rfc/rfc865) Quote of the Day Daemon \ No newline at end of file diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 7af6ff3..b0c17f6 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Cameron Otsuka # Contributor: Cameron Otsuka pkgname=qotdd -pkgver=0.1.0 +pkgver=0.2.0 pkgrel=1 pkgdesc='quote of the day daemon' url='https://github.com/cotsuka/qotdd' diff --git a/src/main.rs b/src/main.rs index 6e271f4..407fda0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use chrono::prelude::*; use rand::prelude::*; use std::fs::File; use std::io::{BufRead, BufReader, Error, ErrorKind, Result, Write}; -use std::path::{Path, PathBuf}; +use std::path::{PathBuf}; use std::net::{TcpListener, TcpStream}; use std::os::fd::{FromRawFd, IntoRawFd}; use std::sync::{Arc, Mutex}; @@ -14,58 +14,17 @@ struct CachedQuote { quote: String, } -#[derive(Clone)] -struct QOTDDService { - quotes_file: PathBuf, - cached_quote: Arc>>, +fn get_date() -> NaiveDate { + let now: DateTime = Local::now(); + return now.date_naive(); } -impl QOTDDService { - fn new(quotes_file: &str) -> Result { - let path = PathBuf::from(quotes_file.to_string()); - if !path.exists() { - return Err(Error::new(ErrorKind::NotFound, format!("quotes file not found: {:?}", path))); - } - - Ok(Self { - quotes_file: path, - cached_quote: Arc::new(Mutex::new(None)), - }) +fn get_quote() -> Result { + let path = PathBuf::from("/etc/qotdd/quotes.txt".to_string()); + if !path.exists() { + return Err(Error::new(ErrorKind::NotFound, format!("quotes file not found: {:?}", path))); } - - fn get_quote(&self) -> Result { - let now: DateTime = Local::now(); - let today = now.date_naive(); - - // check cache - { - let cache_guard = self.cached_quote.lock().unwrap(); - if let Some(ref cached) = *cache_guard { - if cached.date == today { - return Ok(cached.quote.clone()); - } - } - } - - // cache miss: get a new quote - let quote = get_random_line(&self.quotes_file)?; - let new_cached_quote = CachedQuote { - date: today, - quote: quote.clone(), - }; - - // update cache - { - let mut cache_guard = self.cached_quote.lock().unwrap(); - *cache_guard = Some(new_cached_quote); - } - - Ok(quote) - } -} - -fn get_random_line(file_path: &Path) -> Result { - let file = File::open(file_path)?; + let file = File::open(path)?; let reader = BufReader::new(file); let lines: Vec = reader.lines().filter_map(|line| line.ok()).collect(); if lines.is_empty() { @@ -90,20 +49,36 @@ fn get_socket() -> Result { }); } -fn handle_connection(mut stream: TcpStream, service: &QOTDDService) -> Result<()> { - let quote = service.get_quote()?; +fn handle_connection(mut stream: TcpStream, quote: &String) -> Result<()> { stream.write_all(quote.as_bytes())?; stream.flush()?; Ok(()) } -fn run(listener: TcpListener, service: QOTDDService) { +fn main() { + let listener = get_socket().unwrap(); + + let initial_date = get_date(); + let initial_quote = get_quote().unwrap(); + + let initial_cache = Arc::new(Mutex::new(CachedQuote { + date: initial_date, + quote: initial_quote + })); + for stream in listener.incoming() { match stream { Ok(stream) => { - let cloned_service = service.clone(); + let cached_quote = Arc::clone(&initial_cache); thread::spawn(move || { - if let Err(e) = handle_connection(stream, &cloned_service) { + let mut cache = cached_quote.lock().unwrap(); + let today = get_date(); + if cache.date != today { + let new_quote = get_quote().unwrap(); + cache.date = today; + cache.quote = new_quote; + } + if let Err(e) = handle_connection(stream, &cache.quote) { eprintln!("error handling tcp connection: {}", e); } }); @@ -111,10 +86,4 @@ fn run(listener: TcpListener, service: QOTDDService) { Err(e) => eprintln!("error accepting tcp connection: {}", e), } } -} - -fn main() { - let listener = get_socket().unwrap(); - let service = QOTDDService::new("/etc/qotdd/quotes.txt").unwrap(); - run(listener, service); } \ No newline at end of file diff --git a/systemd/qotdd.service b/systemd/qotdd.service index 0b6f85b..0cbc2ce 100644 --- a/systemd/qotdd.service +++ b/systemd/qotdd.service @@ -5,25 +5,38 @@ Wants=network-online.target After=network-online.target qotdd.socket [Service] -Type=simple ExecStart=/usr/bin/qotdd +Type=simple # Security hardening -ProtectHome=yes -PrivateDevices=yes -NoNewPrivileges=yes -MemoryDenyWriteExecute=yes +CapabilityBoundingSet=~CAP_SYS_TIME CAP_SYS_PACCT CAP_KILL CAP_WAKE_ALARM CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_MODULE CAP_BPF CAP_SYS_TTY_CONFIG CAP_SYS_BOOT CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYS_PTRACE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_SYS_ADMIN CAP_SYS_LOG +ConfigurationDirectory=qotdd::ro +DynamicUser=yes LockPersonality=yes -SystemCallFilter=@system-service -RestrictAddressFamilies=AF_UNIX +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +PrivateNetwork=yes +PrivateUsers=yes +ProcSubset=pid +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes ProtectHostname=true -RestrictRealtime=yes -RestrictSUIDSGID=yes ProtectProc=invisible -DynamicUser=yes -ConfigurationDirectory=qotdd::ro Restart=on-failure RestartSec=5s +RestrictAddressFamilies=none +RestrictNamespaces=~user pid net uts mnt cgroup ipc +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service +SystemCallFilter=~@mount @resources @privileged [Install] WantedBy=multi-user.target \ No newline at end of file