From bc940784bdd76f411b895fa449ab4f742267230e Mon Sep 17 00:00:00 2001 From: Reactor Scram Date: Thu, 25 Apr 2024 18:08:06 -0500 Subject: [PATCH] refactor(linux-client): remove FIREZONE_ID from example systemd file (#4714) For tests it doesn't hurt, but this will be used as a template for the systemd service we ship to production, and that can't have the ID there. So I'm also cleaning up a few other problems I noticed: - I wanted to split the service files as part of #4531, so that the GUI Client and headless Client can have separate sandbox rules. e.g, the headless Client won't be allowed to create Unix domain sockets - I'm punting more things to systemd, which allows us to tighten down the sandbox further, e.g. creating `/var/lib/dev.firezone.client` and `/run/dev.firezone.client` for us - Closes #4461 --------- Signed-off-by: Reactor Scram --- .../tunnel/src/device_channel/tun_linux.rs | 12 +++-- rust/headless-client/src/imp_linux.rs | 32 ++++++++----- scripts/tests/linux-group.sh | 5 +- scripts/tests/systemd/dns-systemd-resolved.sh | 6 +-- .../systemd/firezone-client-headless.service | 47 +++++++++++++++++++ ...nt.service => firezone-client-ipc.service} | 11 ++--- 6 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 scripts/tests/systemd/firezone-client-headless.service rename scripts/tests/systemd/{firezone-client.service => firezone-client-ipc.service} (80%) diff --git a/rust/connlib/tunnel/src/device_channel/tun_linux.rs b/rust/connlib/tunnel/src/device_channel/tun_linux.rs index 5d316fd04..595963575 100644 --- a/rust/connlib/tunnel/src/device_channel/tun_linux.rs +++ b/rust/connlib/tunnel/src/device_channel/tun_linux.rs @@ -273,13 +273,15 @@ async fn set_iface_config( res_v4.or(res_v6)?; - match dns_control_method { - None => {} + if let Err(error) = match dns_control_method { + None => Ok(()), Some(DnsControlMethod::EtcResolvConf) => etc_resolv_conf::configure(&dns_config) .await - .map_err(Error::ResolvConf)?, - Some(DnsControlMethod::NetworkManager) => configure_network_manager(&dns_config)?, - Some(DnsControlMethod::Systemd) => configure_systemd_resolved(&dns_config).await?, + .map_err(Error::ResolvConf), + Some(DnsControlMethod::NetworkManager) => configure_network_manager(&dns_config), + Some(DnsControlMethod::Systemd) => configure_systemd_resolved(&dns_config).await, + } { + panic!("Failed to control DNS: {error}"); } // TODO: Having this inside the library is definitely wrong. I think `set_iface_config` diff --git a/rust/headless-client/src/imp_linux.rs b/rust/headless-client/src/imp_linux.rs index 714f69c89..143017ae1 100644 --- a/rust/headless-client/src/imp_linux.rs +++ b/rust/headless-client/src/imp_linux.rs @@ -25,13 +25,6 @@ use tokio_util::codec::LengthDelimitedCodec; const ROOT_GROUP: u32 = 0; const ROOT_USER: u32 = 0; -/// The path for our Unix Domain Socket -/// -/// Docker keeps theirs in `/run` and also appears to use filesystem permissions -/// for security, so we're following their lead. `/run` and `/var/run` are symlinked -/// on some systems, `/run` should be the newer version. -const SOCK_PATH: &str = "/run/firezone-client.sock"; - pub fn default_token_path() -> PathBuf { PathBuf::from("/etc") .join(connlib_shared::BUNDLE_ID) @@ -317,13 +310,27 @@ fn parse_resolvectl_output(s: &str) -> Vec { .collect() } +/// The path for our Unix Domain Socket +/// +/// Docker keeps theirs in `/run` and also appears to use filesystem permissions +/// for security, so we're following their lead. `/run` and `/var/run` are symlinked +/// on some systems, `/run` should be the newer version. +/// +/// Also systemd can create this dir with the `RuntimeDir=` directive which is nice. +fn sock_path() -> PathBuf { + PathBuf::from("/run") + .join(connlib_shared::BUNDLE_ID) + .join("ipc.sock") +} + fn run_debug_ipc_client(_cli: Cli) -> Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { tracing::info!(pid = std::process::id(), "run_debug_ipc_client"); - let stream = UnixStream::connect(SOCK_PATH) + let sock_path = sock_path(); + let stream = UnixStream::connect(&sock_path) .await - .with_context(|| format!("couldn't connect to UDS at {SOCK_PATH}"))?; + .with_context(|| format!("couldn't connect to UDS at {}", sock_path.display()))?; let mut stream = IpcStream::new(stream, LengthDelimitedCodec::new()); stream.send(serde_json::to_string("Hello")?.into()).await?; @@ -346,9 +353,10 @@ async fn ipc_listen() -> Result<()> { .gid; // Remove the socket if a previous run left it there - tokio::fs::remove_file(SOCK_PATH).await.ok(); - let listener = UnixListener::bind(SOCK_PATH).context("Couldn't bind UDS")?; - std::os::unix::fs::chown(SOCK_PATH, Some(ROOT_USER), Some(fz_gid.into())) + let sock_path = sock_path(); + tokio::fs::remove_file(&sock_path).await.ok(); + let listener = UnixListener::bind(&sock_path).context("Couldn't bind UDS")?; + std::os::unix::fs::chown(&sock_path, Some(ROOT_USER), Some(fz_gid.into())) .context("can't set firezone as the group for the UDS")?; sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; diff --git a/scripts/tests/linux-group.sh b/scripts/tests/linux-group.sh index 3fb9afa51..5598bd905 100755 --- a/scripts/tests/linux-group.sh +++ b/scripts/tests/linux-group.sh @@ -7,18 +7,17 @@ source "./scripts/tests/lib.sh" BINARY_NAME=firezone-linux-client FZ_GROUP="firezone" -SERVICE_NAME=firezone-client +SERVICE_NAME=firezone-client-ipc export RUST_LOG=info # Copy the Linux Client out of the build dir -ls . ./rust ./rust/target ./rust/target/debug sudo cp "rust/target/debug/firezone-headless-client" "/usr/bin/$BINARY_NAME" sudo cp "scripts/tests/systemd/$SERVICE_NAME.service" /usr/lib/systemd/system/ # The firezone group must exist before the daemon starts sudo groupadd "$FZ_GROUP" -sudo systemctl start "$SERVICE_NAME" +sudo systemctl start "$SERVICE_NAME" || systemctl status "$SERVICE_NAME" # Add ourselves to the firezone group sudo gpasswd --add "$USER" "$FZ_GROUP" diff --git a/scripts/tests/systemd/dns-systemd-resolved.sh b/scripts/tests/systemd/dns-systemd-resolved.sh index f247537d5..67f4b1e85 100755 --- a/scripts/tests/systemd/dns-systemd-resolved.sh +++ b/scripts/tests/systemd/dns-systemd-resolved.sh @@ -4,7 +4,7 @@ source "./scripts/tests/lib.sh" BINARY_NAME=firezone-linux-client -SERVICE_NAME=firezone-client +SERVICE_NAME=firezone-client-headless # Copy the Linux Client out of its container docker compose exec client cat firezone-linux-client > "$BINARY_NAME" @@ -22,12 +22,12 @@ HTTPBIN=dns.httpbin DOCKER_IFACE="docker0" FZ_IFACE="tun-firezone" -# Accessing a resource should fail before the client is up +echo "# Accessing a resource should fail before the client is up" # Force curl to try the Firezone interface. I can't block off the Docker interface yet # because it may be needed for the client to reach the portal. curl --interface "$FZ_IFACE" $HTTPBIN/get && exit 1 -# Start Firezone +echo "# Start Firezone" resolvectl dns tun-firezone && exit 1 stat /usr/bin/firezone-linux-client sudo systemctl start "$SERVICE_NAME" || systemctl status "$SERVICE_NAME" diff --git a/scripts/tests/systemd/firezone-client-headless.service b/scripts/tests/systemd/firezone-client-headless.service new file mode 100644 index 000000000..53d9a6e1c --- /dev/null +++ b/scripts/tests/systemd/firezone-client-headless.service @@ -0,0 +1,47 @@ +[Unit] +Description=Firezone Client + +[Service] +AmbientCapabilities=CAP_NET_ADMIN +CapabilityBoundingSet=CAP_NET_ADMIN +DeviceAllow=/dev/net/tun +LockPersonality=true +MemoryDenyWriteExecute=true +NoNewPrivileges=true +PrivateMounts=true +PrivateTmp=true +# We need to be real root, not just root in our cgroup +PrivateUsers=false +ProcSubset=pid +ProtectClock=true +ProtectControlGroups=true +ProtectHome=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +# Docs say it's useless when running as root, but defense-in-depth +ProtectProc=invisible +ProtectSystem=strict +# Netlink needed for the tunnel interface, Unix needed for `systemd-resolved` +RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +StateDirectory=dev.firezone.client +SystemCallArchitectures=native +# TODO: Minimize +SystemCallFilter=@aio @basic-io @file-system @io-event @network-io @signal @system-service +UMask=077 + +Environment="FIREZONE_API_URL=ws://localhost:8081" +Environment="FIREZONE_DNS_CONTROL=systemd-resolved" +Environment="RUST_LOG=info" + +ExecStart=firezone-linux-client standalone +Type=notify +# Unfortunately we may need root to control DNS +User=root + +[Install] +WantedBy=default.target diff --git a/scripts/tests/systemd/firezone-client.service b/scripts/tests/systemd/firezone-client-ipc.service similarity index 80% rename from scripts/tests/systemd/firezone-client.service rename to scripts/tests/systemd/firezone-client-ipc.service index 72653cdfd..19cd389e3 100644 --- a/scripts/tests/systemd/firezone-client.service +++ b/scripts/tests/systemd/firezone-client-ipc.service @@ -3,7 +3,6 @@ Description=Firezone Client [Service] AmbientCapabilities=CAP_NET_ADMIN -# TODO: Get rid of `CAP_CHOWN` here by asking systemd to make our runtime dir on our behalf CapabilityBoundingSet=CAP_CHOWN CAP_NET_ADMIN DeviceAllow=/dev/net/tun LockPersonality=true @@ -23,23 +22,23 @@ ProtectKernelModules=true ProtectKernelTunables=true # Docs say it's useless when running as root, but defense-in-depth ProtectProc=invisible -ProtectSystem=full +ProtectSystem=strict RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true +RuntimeDirectory=dev.firezone.client +StateDirectory=dev.firezone.client SystemCallArchitectures=native # TODO: Minimize SystemCallFilter=@aio @basic-io @file-system @io-event @ipc @network-io @signal @system-service -UMask=177 +UMask=077 Environment="FIREZONE_API_URL=ws://localhost:8081" Environment="FIREZONE_DNS_CONTROL=systemd-resolved" -Environment="FIREZONE_ID=D0455FDE-8F65-4960-A778-B934E4E85A5F" Environment="RUST_LOG=info" -# TODO: Make subcommands explicit once PR #4628 merges -ExecStart=firezone-linux-client +ExecStart=firezone-linux-client ipc-service Type=notify # Unfortunately we may need root to control DNS User=root