feat(gateway): support systemd credentials (#10538)

For more permanent Gateway installations, or ones that are managed
through something else other than our install script, it is useful to
define the Gateway's token outside the systemd unit file.

Systemd provides support for credentials via the `LoadCredential` and
`LoadCredentialEncrypted` instructions. We just need a tiny bit of glue
code in the Gateway to actually use that if it is set.

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Thomas Eizinger
2025-10-14 11:07:49 +11:00
committed by GitHub
parent 4930aa7956
commit 038aa6b590
4 changed files with 59 additions and 8 deletions

1
rust/Cargo.lock generated
View File

@@ -2428,6 +2428,7 @@ dependencies = [
"snownet",
"socket-factory",
"static_assertions",
"tempfile",
"thiserror 2.0.16",
"tokio",
"tracing",

View File

@@ -56,6 +56,7 @@ dns-lookup = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true, features = ["std"] }
tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -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=<gateway_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=<gateway_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.

View File

@@ -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<String>) -> Result<String> {
Ok(device_id.id)
}
async fn read_systemd_credential(name: &str) -> Result<SecretString> {
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<String>,
token: Option<SecretString>,
/// Friendly name to display in the UI
#[arg(short = 'n', long, env = "FIREZONE_NAME")]
firezone_name: Option<String>,
@@ -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");
}
}
}