diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ec17bf805..d62366a7e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2428,6 +2428,7 @@ dependencies = [ "snownet", "socket-factory", "static_assertions", + "tempfile", "thiserror 2.0.16", "tokio", "tracing", diff --git a/rust/gateway/Cargo.toml b/rust/gateway/Cargo.toml index b673a8a93..f11226791 100644 --- a/rust/gateway/Cargo.toml +++ b/rust/gateway/Cargo.toml @@ -56,6 +56,7 @@ dns-lookup = { workspace = true } [dev-dependencies] serde_json = { workspace = true, features = ["std"] } +tempfile = { workspace = true } [lints] workspace = true diff --git a/rust/gateway/README.md b/rust/gateway/README.md index 761b5d039..570cd4e0f 100644 --- a/rust/gateway/README.md +++ b/rust/gateway/README.md @@ -15,9 +15,9 @@ Linux host: 1. Generate a new Gateway token from the "Gateways" section of the admin portal and save it in your secrets manager. -1. Ensure the `FIREZONE_TOKEN=` environment variable is set - securely in your Gateway's shell environment. The Gateway requires this - variable at startup. +1. Provide the token to the Gateway using one of these methods: + - Set the `FIREZONE_TOKEN=` environment variable + - Set a [systemd credential](https://systemd.io/CREDENTIALS) named `FIREZONE_TOKEN`. 1. Set `FIREZONE_ID` to a unique string to identify this gateway in the portal, e.g. `export FIREZONE_ID=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1)`. The Gateway requires this variable at startup. We recommend this to be a 64 character hex string. diff --git a/rust/gateway/src/main.rs b/rust/gateway/src/main.rs index caf674652..2f076b3fb 100644 --- a/rust/gateway/src/main.rs +++ b/rust/gateway/src/main.rs @@ -1,5 +1,7 @@ +#![cfg_attr(test, allow(clippy::unwrap_used))] + use crate::eventloop::{Eventloop, PHOENIX_TOPIC}; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use backoff::ExponentialBackoffBuilder; use clap::Parser; use firezone_bin_shared::{ @@ -19,9 +21,9 @@ use phoenix_channel::LoginUrl; use phoenix_channel::get_user_agent; use phoenix_channel::PhoenixChannel; -use secrecy::Secret; -use std::process::ExitCode; +use secrecy::{Secret, SecretString}; use std::{collections::BTreeSet, path::Path}; +use std::{path::PathBuf, process::ExitCode}; use std::{sync::Arc, time::Duration}; use tracing_subscriber::layer; use tun::Tun; @@ -119,6 +121,13 @@ async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result<()> { let firezone_id = get_firezone_id(cli.firezone_id.clone()).await .context("Couldn't read FIREZONE_ID or write it to disk: Please provide it through the env variable or provide rw access to /var/lib/firezone/")?; + let token = match cli.token.clone() { + Some(token) => token, + None => read_systemd_credential("FIREZONE_TOKEN") + .await + .context("Failed to read `FIREZONE_TOKEN` systemd credential")?, + }; + if cli.is_telemetry_allowed() { telemetry .start( @@ -162,7 +171,7 @@ async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result<()> { opentelemetry::global::set_meter_provider(provider); } - let login = LoginUrl::gateway(cli.api_url, &cli.token, firezone_id, cli.firezone_name) + let login = LoginUrl::gateway(cli.api_url, &token, firezone_id, cli.firezone_name) .context("Failed to construct URL for logging into portal")?; let resolv_conf = resolv_conf::Config::parse( @@ -254,6 +263,16 @@ async fn get_firezone_id(env_id: Option) -> Result { Ok(device_id.id) } +async fn read_systemd_credential(name: &str) -> Result { + let Ok(creds_dir) = std::env::var("CREDENTIALS_DIRECTORY") else { + bail!("`CREDENTIALS_DIRECTORY` not provided") + }; + let path = PathBuf::from(creds_dir).join(name); + let content = tokio::fs::read_to_string(&path).await?; + + Ok(SecretString::new(content)) +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { @@ -267,7 +286,7 @@ struct Cli { api_url: Url, /// Token generated by the portal to authorize websocket connection. #[arg(env = "FIREZONE_TOKEN")] - token: Secret, + token: Option, /// Friendly name to display in the UI #[arg(short = 'n', long, env = "FIREZONE_NAME")] firezone_name: Option, @@ -400,3 +419,33 @@ impl ValidateChecksumAdapter { Box::new(Self { inner }) } } + +#[cfg(test)] +mod tests { + use super::*; + use secrecy::ExposeSecret as _; + use tempfile::TempDir; + + #[tokio::test] + async fn get_firezone_token_from_systemd_credential_with_credentials_directory() { + // Create a temporary directory to simulate CREDENTIALS_DIRECTORY + let temp_dir = TempDir::new().unwrap(); + let cred_path = temp_dir.path().join("FIREZONE_TOKEN"); + + // Write token to credential file + std::fs::write(cred_path, "systemd-token").unwrap(); + + // Set CREDENTIALS_DIRECTORY environment variable + unsafe { + std::env::set_var("CREDENTIALS_DIRECTORY", temp_dir.path()); + } + + let result = read_systemd_credential("FIREZONE_TOKEN").await.unwrap(); + assert_eq!(result.expose_secret(), "systemd-token"); + + // Clean up + unsafe { + std::env::remove_var("CREDENTIALS_DIRECTORY"); + } + } +}