feat(gateway): create debian package (#10537)

With this PR we add `cargo-deb` to our CI pipeline and build a debian
package for the Gateway. The debian package comes with several
configuration files that make it easy for admins to start and maintain a
Gateway installation:

- The embedded systemd unit file is essentially the same one as what we
currently install with the install script with some minor modifications.
- The token is read from `/etc/firezone/gateway-token` and passed as a
systemd credential. This allows us to set the permissions for this file
to `0400` and have it owned by `root:root`.
	- The configuration is read from `/etc/firezone/gateway-env`.
- Both of these changes basically mean the user should never need to
touch the unit file itself.
- The `sysusers` configuration file ensures the `firezone` user and
group are present on the system.
- The `tmpfiles` configuration file ensures the necessary directories
are present.

All of the above is automatically installed and configured using the
post-installation script which is called by `apt` once the package is
installed.

In addition to the Gateway, we also package a first version of the
`firezone-cli`. Right now, `firezone-cli` (installed as `firezone`) has
three subcommands:

- `gateway authenticate`: Asks for the Gateway's token and installs it
at `/etc/firezone/gateway-token`. The user doesn't have to know how we
manage this token and can trust that we are using safe defaults.
- `gateway enable`: Enables and starts the systemd service.
- `gateway disable`: Disables the systemd service.

Right now, the `.deb` file is only uploaded to the preview APT
repository and not attached to the release. It should therefore not yet
be user-visible unless somebody pokes around a lot, meaning we can defer
documentation to a later PR and start testing it from the preview
repository for our own purposes.

Related: #10598
Resolves: #8484 
Resolves: #10681
This commit is contained in:
Thomas Eizinger
2025-10-24 16:14:58 +11:00
committed by GitHub
parent f4ce6f12a0
commit 0d2ddd8497
11 changed files with 363 additions and 2 deletions

16
rust/cli/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "firezone-cli"
version = "1.0.0"
edition = { workspace = true }
license = { workspace = true }
description = "CLI for managing Firezone installations"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
[target.'cfg(target_os = "linux")'.dependencies]
nix = { workspace = true, features = ["user"] }
[lints]
workspace = true

165
rust/cli/src/main.rs Normal file
View File

@@ -0,0 +1,165 @@
#![expect(clippy::print_stdout, reason = "We are a CLI.")]
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
const ETC_FIREZONE_GATEWAY_TOKEN: &str = "/etc/firezone/gateway-token";
fn main() -> Result<()> {
let cli = Cli::parse();
use Component::*;
use GatewayCommand::*;
match cli.component {
Gateway(Authenticate { replace }) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
if let Ok(existing) = std::fs::read_to_string(ETC_FIREZONE_GATEWAY_TOKEN)
&& !existing.trim().is_empty()
&& !replace
{
anyhow::bail!(
"Found existing token at {ETC_FIREZONE_GATEWAY_TOKEN}, use --replace to overwrite"
);
}
let mut token = String::with_capacity(512); // Our tokens are ~270 characters, grab the next power of 2.
loop {
println!("Paste the token from the portal's deploy page:");
let num_bytes = std::io::stdin()
.read_line(&mut token)
.context("Failed to read token from stdin")?;
if num_bytes == 0 || token.trim().is_empty() {
continue;
}
break;
}
install_firezone_gateway_token(token)?;
println!("Successfully installed token");
println!("Tip: You can now start the Gateway with `firezone gateway enable`");
}
Gateway(Enable) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
enable_gateway_service().context("Failed to enable `firezone-gateway.service`")?;
println!("Successfully enabled `firezone-gateway.service`");
}
Gateway(Disable) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
disable_gateway_service().context("Failed to disable `firezone-gateway.service`")?;
println!("Successfully disabled `firezone-gateway.service`");
}
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(name = "firezone", bin_name = "firezone", about, long_about = None)]
struct Cli {
#[command(subcommand)]
component: Component,
}
#[derive(Debug, Subcommand)]
enum Component {
#[command(subcommand)]
Gateway(GatewayCommand),
}
#[derive(Debug, Subcommand)]
enum GatewayCommand {
/// Securely store the Gateway's token on disk.
Authenticate {
/// If an existing token is found, replace it.
#[arg(long, default_value_t = false)]
replace: bool,
},
/// Enable the Gateway's systemd service.
Enable,
/// Disable the Gateway's systemd service.
Disable,
}
#[cfg(target_os = "linux")]
fn is_root() -> bool {
nix::unistd::Uid::current().is_root()
}
#[cfg(not(target_os = "linux"))]
fn is_root() -> bool {
true
}
#[cfg(target_os = "linux")]
fn install_firezone_gateway_token(token: String) -> Result<()> {
std::fs::write(ETC_FIREZONE_GATEWAY_TOKEN, token)
.with_context(|| format!("Failed to write token to `{ETC_FIREZONE_GATEWAY_TOKEN}`"))?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn install_firezone_gateway_token(token: String) -> Result<()> {
anyhow::bail!("Not implemented")
}
#[cfg(target_os = "linux")]
fn enable_gateway_service() -> Result<()> {
use std::process::Command;
let output = Command::new("systemctl")
.arg("enable")
.arg("--now")
.arg("firezone-gateway.service")
.output()?;
anyhow::ensure!(
output.status.success(),
"`systemctl enable` exited with {}",
output.status
);
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn enable_gateway_service() -> Result<()> {
anyhow::bail!("Not implemented")
}
#[cfg(target_os = "linux")]
fn disable_gateway_service() -> Result<()> {
use std::process::Command;
let output = Command::new("systemctl")
.arg("disable")
.arg("firezone-gateway.service")
.output()?;
anyhow::ensure!(
output.status.success(),
"`systemctl disable` exited with {}",
output.status
);
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn disable_gateway_service() -> Result<()> {
anyhow::bail!("Not implemented")
}