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 <thomas@eizinger.io>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Thomas Eizinger
2024-12-29 22:45:56 +01:00
committed by GitHub
parent 1c2c350b8f
commit 26824fb3c7
7 changed files with 62 additions and 2 deletions

12
rust/Cargo.lock generated
View File

@@ -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",

View File

@@ -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 }

View File

@@ -65,7 +65,7 @@ impl TunDeviceManager {
///
/// Panics if called without a Tokio runtime.
pub fn new(mtu: usize, num_threads: usize) -> Result<Self> {
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 };

View File

@@ -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 }

View File

@@ -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<ExitCode> {
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,

View File

@@ -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

View File

@@ -15,6 +15,11 @@ export default function Gateway() {
Fixes an issue where ICMPv6's `PacketTooBig' errors were not correctly
translated by the NAT64 module.
</ChangeItem>
<ChangeItem pull="7565">
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`.
</ChangeItem>
</Unreleased>
<Entry version="1.4.2" date={new Date("2024-12-13")}>
<ChangeItem pull="7210">