mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
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 <ReactorScram@users.noreply.github.com>
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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<IpAddr> {
|
||||
.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])?;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
47
scripts/tests/systemd/firezone-client-headless.service
Normal file
47
scripts/tests/systemd/firezone-client-headless.service
Normal file
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user