From 26824fb3c73097c4f18a18d579a1c5a23a6c3df6 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Sun, 29 Dec 2024 22:45:56 +0100 Subject: [PATCH] fix(gateway): check if we run with correct permissions (#7565) The gateway needs either the `CAP_NET_ADMIN` capability or run as `root` in order to access the TUN device as well as configure routes via `netlink`. Running without either leads to "Permission denied" errors at runtime. It is good to fail early in these kind of situations. By checking for this capability early on during startup, these should no longer surface later. As a bonus, we won't receive (unactionable) Sentry alerts. Resolves: #7559. --------- Signed-off-by: Thomas Eizinger Co-authored-by: Jamil --- rust/Cargo.lock | 12 +++++++++ rust/Cargo.toml | 1 + .../src/tun_device_manager/linux.rs | 2 +- rust/gateway/Cargo.toml | 2 ++ rust/gateway/src/main.rs | 25 ++++++++++++++++++- website/src/app/kb/deploy/gateways/readme.mdx | 17 +++++++++++++ website/src/components/Changelog/Gateway.tsx | 5 ++++ 7 files changed, 62 insertions(+), 2 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ec5862029..4d42cecb9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -752,6 +752,16 @@ dependencies = [ "serde", ] +[[package]] +name = "caps" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + [[package]] name = "cargo-platform" version = "0.1.8" @@ -1952,6 +1962,7 @@ dependencies = [ "async-trait", "backoff", "boringtun", + "caps", "chrono", "clap", "connlib-model", @@ -1967,6 +1978,7 @@ dependencies = [ "ip-packet", "ip_network", "libc", + "nix 0.29.0", "phoenix-channel", "rustls", "secrecy", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9b3ffbd0c..b73900acf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -59,6 +59,7 @@ glob = "0.3.1" hex = "0.4.3" hex-display = "0.3.0" hex-literal = "0.4.1" +caps = "0.5.5" humantime = "2.1" ip_network = { version = "0.4", default-features = false } ip_network_table = { version = "0.2", default-features = false } diff --git a/rust/bin-shared/src/tun_device_manager/linux.rs b/rust/bin-shared/src/tun_device_manager/linux.rs index 7716d5f92..e1efa9f71 100644 --- a/rust/bin-shared/src/tun_device_manager/linux.rs +++ b/rust/bin-shared/src/tun_device_manager/linux.rs @@ -65,7 +65,7 @@ impl TunDeviceManager { /// /// Panics if called without a Tokio runtime. pub fn new(mtu: usize, num_threads: usize) -> Result { - let (cxn, handle, _) = new_connection()?; + let (cxn, handle, _) = new_connection().context("Failed to create netlink connection")?; let task = tokio::spawn(cxn); let connection = Connection { handle, task }; diff --git a/rust/gateway/Cargo.toml b/rust/gateway/Cargo.toml index 716e500c3..94934135c 100644 --- a/rust/gateway/Cargo.toml +++ b/rust/gateway/Cargo.toml @@ -11,6 +11,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } backoff = { workspace = true } boringtun = { workspace = true } +caps = { workspace = true } chrono = { workspace = true } clap = { workspace = true } connlib-model = { workspace = true } @@ -26,6 +27,7 @@ futures-bounded = { workspace = true } ip-packet = { workspace = true } ip_network = { workspace = true } libc = { workspace = true, features = ["std", "const-extern-fn", "extra_traits"] } +nix = { workspace = true } phoenix-channel = { workspace = true } rustls = { workspace = true } secrecy = { workspace = true } diff --git a/rust/gateway/src/main.rs b/rust/gateway/src/main.rs index 9d5cfff2a..180d2422a 100644 --- a/rust/gateway/src/main.rs +++ b/rust/gateway/src/main.rs @@ -1,6 +1,7 @@ use crate::eventloop::{Eventloop, PHOENIX_TOPIC}; use anyhow::{Context, Result}; use backoff::ExponentialBackoffBuilder; +use caps::{CapSet, Capability}; use clap::Parser; use firezone_bin_shared::{ http_health_check, @@ -31,11 +32,20 @@ mod eventloop; const ID_PATH: &str = "/var/lib/firezone/gateway_id"; fn main() -> ExitCode { + let cli = Cli::parse(); + + #[expect(clippy::print_stderr, reason = "No logger has been set up yet")] + if !has_necessary_permissions() && !cli.no_check { + eprintln!( + "firezone-gateway needs to be executed as `root` or with the `CAP_NET_ADMIN` capability.\nSee https://www.firezone.dev/kb/deploy/gateways#permissions for details." + ); + return ExitCode::FAILURE; + } + rustls::crypto::ring::default_provider() .install_default() .expect("Calling `install_default` only once per process should always succeed"); - let cli = Cli::parse(); let mut telemetry = Telemetry::default(); if cli.is_telemetry_allowed() { telemetry.start( @@ -73,6 +83,15 @@ fn main() -> ExitCode { } } +#[must_use] +fn has_necessary_permissions() -> bool { + let is_root = nix::unistd::Uid::current().is_root(); + let has_net_admin = + caps::has_cap(None, CapSet::Effective, Capability::CAP_NET_ADMIN).is_ok_and(|b| b); + + is_root || has_net_admin +} + async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result { firezone_logging::setup_global_subscriber(layer::Identity::default()) .context("Failed to set up logging")?; @@ -184,6 +203,10 @@ struct Cli { #[arg(long, env = "FIREZONE_NO_TELEMETRY", default_value_t = false)] no_telemetry: bool, + /// Don't preemtively check permissions. + #[arg(long, default_value_t = false)] + no_check: bool, + #[command(flatten)] health_check: http_health_check::HealthCheckArgs, diff --git a/website/src/app/kb/deploy/gateways/readme.mdx b/website/src/app/kb/deploy/gateways/readme.mdx index 69d22a8d1..e0db8a44f 100644 --- a/website/src/app/kb/deploy/gateways/readme.mdx +++ b/website/src/app/kb/deploy/gateways/readme.mdx @@ -63,6 +63,23 @@ you'll need to make sure the following outbound traffic is allowed: | github.com, www.firezone.dev | Varies | `443` | HTTPS | Only required for [Gateway upgrades](/kb/administer/upgrading). | | sentry.io | Varies | `443` | HTTPS | Crash-reporting, see [Telemetry](#telemetry) | +#### Permissions + +In order to function correctly, Gateways need access to several parts of the +Linux system: + +1. The TUN device as `/dev/net/tun` +1. Permissions to open new UDP sockets +1. Permissions to add and remove routes via `netlink` + +Typically, it is enough to run Gateways with the `CAP_NET_ADMIN` capability. +Alternatively, you can run them as `root`. + +Gateways will check on startup for these two conditions and fail if neither are +met. You can skip these permission checks by passing `--no-check`. This is only +advisable in case you have configured access in ways not covered by these +checks. + ## Where to deploy Gateways Ideally, Gateways should be deployed as close to the Resources they're serving diff --git a/website/src/components/Changelog/Gateway.tsx b/website/src/components/Changelog/Gateway.tsx index 500c681c3..69e67924e 100644 --- a/website/src/components/Changelog/Gateway.tsx +++ b/website/src/components/Changelog/Gateway.tsx @@ -15,6 +15,11 @@ export default function Gateway() { Fixes an issue where ICMPv6's `PacketTooBig' errors were not correctly translated by the NAT64 module. + + Fails early in case the binary is not started as `root` or with the + `CAP_NET_ADMIN` capability. The check can be skipped with + `--no-check`. +