mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
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:
30
.github/workflows/_data-plane.yml
vendored
30
.github/workflows/_data-plane.yml
vendored
@@ -264,7 +264,7 @@ jobs:
|
||||
run: ${{ matrix.arch.install_dependencies }}
|
||||
- uses: taiki-e/install-action@d31232495ad76f47aad66e3501e47780b49f0f3e # v2.57.5
|
||||
with:
|
||||
tool: bpf-linker
|
||||
tool: bpf-linker,cargo-deb
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build binaries
|
||||
@@ -318,6 +318,12 @@ jobs:
|
||||
--overwrite true \
|
||||
--no-progress \
|
||||
--connection-string "${{ secrets.AZURERM_ARTIFACTS_CONNECTION_STRING }}"
|
||||
- name: Create Firezone Gateway .deb package
|
||||
if: ${{ inputs.profile == 'release' && matrix.stage == 'release' && matrix.name.artifact == 'firezone-gateway' }}
|
||||
run: |
|
||||
cargo build --bin firezone-cli --release --target ${{ matrix.arch.target }}
|
||||
cargo deb --package firezone-gateway --target ${{ matrix.arch.target }} --no-build --no-strip
|
||||
cp target/debian/*.deb "$BINARY_DEST_PATH".deb
|
||||
- name: Upload Release Assets
|
||||
if: ${{ inputs.profile == 'release' && matrix.stage == 'release' && matrix.name.release_name && github.event_name == 'workflow_dispatch' && github.ref_name == 'main' }}
|
||||
env:
|
||||
@@ -339,8 +345,24 @@ jobs:
|
||||
gh release upload ${{ matrix.name.release_name }} \
|
||||
"$BINARY_DEST_PATH" \
|
||||
"$BINARY_DEST_PATH".sha256sum.txt \
|
||||
# "$BINARY_DEST_PATH".deb \ # Enable this once we have all the necessary documentation in place.
|
||||
"$clobber" \
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
az storage blob upload-batch \
|
||||
--destination apt \
|
||||
--source . \
|
||||
--pattern "*.deb" \
|
||||
--destination-path import-preview \
|
||||
--overwrite \
|
||||
--no-progress \
|
||||
--connection-string "${{ secrets.AZURERM_ARTIFACTS_CONNECTION_STRING }}"
|
||||
- name: Upload `.deb` artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ env.BINARY_DEST_PATH }}.deb
|
||||
path: rust/${{ env.BINARY_DEST_PATH }}.deb
|
||||
retention-days: 1
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -475,3 +497,9 @@ jobs:
|
||||
# shellcheck disable=SC2086 # $tags and $sources must be split by whitespace
|
||||
docker buildx imagetools create $tags $sources
|
||||
docker buildx imagetools inspect "${{ steps.login.outputs.registry }}/firezone/${{ matrix.image_prefix && format('{0}/', matrix.image_prefix) || '' }}${{ matrix.image.name }}"
|
||||
|
||||
regenerate-apt-index:
|
||||
needs: data-plane-linux
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.ref_name == 'main' }}
|
||||
uses: ./.github/workflows/_apt.yml
|
||||
secrets: inherit
|
||||
|
||||
9
rust/Cargo.lock
generated
9
rust/Cargo.lock
generated
@@ -2389,6 +2389,15 @@ dependencies = [
|
||||
"zbus 5.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "firezone-cli"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"nix 0.30.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "firezone-gateway"
|
||||
version = "1.4.18"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"bin-shared",
|
||||
"cli",
|
||||
"client-ffi",
|
||||
"client-shared",
|
||||
"connlib/bufferpool",
|
||||
|
||||
16
rust/cli/Cargo.toml
Normal file
16
rust/cli/Cargo.toml
Normal 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
165
rust/cli/src/main.rs
Normal 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")
|
||||
}
|
||||
@@ -4,7 +4,24 @@ name = "firezone-gateway"
|
||||
version = "1.4.18"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
description = "Gateway for the Firezone zero-trust access product."
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Firezone, Inc. <support@firezone.dev>"
|
||||
copyright = "2025 Firezone, Inc."
|
||||
maintainer-scripts = "debian/"
|
||||
systemd-units = { enable = false }
|
||||
extended-description = "" # Empty string to avoid embedding the README
|
||||
revision = ""
|
||||
section = "Network"
|
||||
assets = [
|
||||
["target/release/firezone-gateway", "usr/bin/", "755"],
|
||||
["target/release/firezone-cli", "usr/bin/firezone", "755"],
|
||||
["debian/firezone-gateway-init.sh", "usr/bin/firezone-gateway-init", "755"],
|
||||
["debian/firezone-gateway.sysusers", "usr/lib/sysusers.d/firezone-gateway.conf", "644"],
|
||||
["debian/firezone-gateway.tmpfiles", "usr/lib/tmpfiles.d/firezone-gateway.conf", "644"],
|
||||
]
|
||||
depends = 'iptables'
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
20
rust/gateway/debian/firezone-gateway-init.sh
Normal file
20
rust/gateway/debian/firezone-gateway-init.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ue
|
||||
|
||||
# Enable masquerading for Firezone tunnel traffic
|
||||
iptables -C FORWARD -i tun-firezone -j ACCEPT >/dev/null 2>&1 || iptables -I FORWARD 1 -i tun-firezone -j ACCEPT
|
||||
iptables -C FORWARD -o tun-firezone -j ACCEPT >/dev/null 2>&1 || iptables -I FORWARD 1 -o tun-firezone -j ACCEPT
|
||||
iptables -t nat -C POSTROUTING -s 100.64.0.0/11 -o e+ -j MASQUERADE >/dev/null 2>&1 || iptables -t nat -A POSTROUTING -s 100.64.0.0/11 -o e+ -j MASQUERADE
|
||||
iptables -t nat -C POSTROUTING -s 100.64.0.0/11 -o w+ -j MASQUERADE >/dev/null 2>&1 || iptables -t nat -A POSTROUTING -s 100.64.0.0/11 -o w+ -j MASQUERADE
|
||||
ip6tables -C FORWARD -i tun-firezone -j ACCEPT >/dev/null 2>&1 || ip6tables -I FORWARD 1 -i tun-firezone -j ACCEPT
|
||||
ip6tables -C FORWARD -o tun-firezone -j ACCEPT >/dev/null 2>&1 || ip6tables -I FORWARD 1 -o tun-firezone -j ACCEPT
|
||||
ip6tables -t nat -C POSTROUTING -s fd00:2021:1111::/107 -o e+ -j MASQUERADE >/dev/null 2>&1 || ip6tables -t nat -A POSTROUTING -s fd00:2021:1111::/107 -o e+ -j MASQUERADE
|
||||
ip6tables -t nat -C POSTROUTING -s fd00:2021:1111::/107 -o w+ -j MASQUERADE >/dev/null 2>&1 || ip6tables -t nat -A POSTROUTING -s fd00:2021:1111::/107 -o w+ -j MASQUERADE
|
||||
|
||||
# Enable packet forwarding for IPv4 and IPv6
|
||||
sysctl -w net.ipv4.ip_forward=1
|
||||
sysctl -w net.ipv4.conf.all.src_valid_mark=1
|
||||
sysctl -w net.ipv6.conf.all.disable_ipv6=0
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1
|
||||
sysctl -w net.ipv6.conf.default.forwarding=1
|
||||
76
rust/gateway/debian/firezone-gateway.service
Normal file
76
rust/gateway/debian/firezone-gateway.service
Normal file
@@ -0,0 +1,76 @@
|
||||
[Unit]
|
||||
Description=Firezone Gateway
|
||||
After=network.target
|
||||
Documentation=https://www.firezone.dev/kb
|
||||
|
||||
[Service]
|
||||
|
||||
# DO NOT EDIT ANY OF THE BELOW BY HAND. USE "systemctl edit firezone-gateway" INSTEAD TO CUSTOMIZE.
|
||||
# Most configuration should go as environment variables into `/etc/firezone/gateway-env`.
|
||||
# The access token should be in `/etc/firezone/gateway-token`.
|
||||
|
||||
Type=simple
|
||||
User=firezone
|
||||
Group=firezone
|
||||
PermissionsStartOnly=true
|
||||
SyslogIdentifier=firezone-gateway
|
||||
|
||||
LoadCredential=FIREZONE_TOKEN:/etc/firezone/gateway-token
|
||||
EnvironmentFile=/etc/firezone/gateway-preset-env
|
||||
EnvironmentFile=/etc/firezone/gateway-env
|
||||
|
||||
ExecStartPre=/usr/bin/firezone-gateway-init
|
||||
ExecStart=/usr/bin/firezone-gateway
|
||||
|
||||
# Restart on failure
|
||||
TimeoutStartSec=15s
|
||||
TimeoutStopSec=15s
|
||||
Restart=always
|
||||
RestartSec=7
|
||||
|
||||
#####################
|
||||
# HARDENING OPTIONS #
|
||||
#####################
|
||||
|
||||
# Give the service its own private /tmp directory.
|
||||
PrivateTmp=true
|
||||
|
||||
# Mount the system directories read-only (except those explicitly allowed).
|
||||
ProtectSystem=full
|
||||
|
||||
# Make users' home directories read-only.
|
||||
ProtectHome=read-only
|
||||
|
||||
# Disallow gaining new privileges (e.g. via execve() of setuid binaries).
|
||||
NoNewPrivileges=true
|
||||
|
||||
# Disallow the creation of new namespaces.
|
||||
RestrictNamespaces=yes
|
||||
|
||||
# Prevent memory from being both writable and executable.
|
||||
MemoryDenyWriteExecute=true
|
||||
|
||||
# Prevent the service from calling personality(2) to change process execution domain.
|
||||
LockPersonality=true
|
||||
|
||||
# Restrict the set of allowed address families.
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK
|
||||
|
||||
# Allow the process to have CAP_NET_ADMIN (needed for network administration)
|
||||
# while restricting it to only that capability.
|
||||
AmbientCapabilities=CAP_NET_ADMIN
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN
|
||||
|
||||
# Make some sensitive paths inaccessible.
|
||||
InaccessiblePaths=/root /home
|
||||
|
||||
# Set resource limits
|
||||
LimitNOFILE=4096
|
||||
LimitNPROC=512
|
||||
LimitCORE=0
|
||||
|
||||
# Set a sane system call filter
|
||||
SystemCallFilter=@system-service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
3
rust/gateway/debian/firezone-gateway.sysusers
Normal file
3
rust/gateway/debian/firezone-gateway.sysusers
Normal file
@@ -0,0 +1,3 @@
|
||||
# Declarative configuration for sysusers.d, creating a locked down system user named `firezone` and a corresponding group.
|
||||
|
||||
u! firezone
|
||||
6
rust/gateway/debian/firezone-gateway.tmpfiles
Normal file
6
rust/gateway/debian/firezone-gateway.tmpfiles
Normal file
@@ -0,0 +1,6 @@
|
||||
#Type Path Mode User Group Age Argument
|
||||
|
||||
d /etc/firezone 0755 firezone firezone
|
||||
f /etc/firezone/gateway-env 0644 firezone firezone -
|
||||
f /etc/firezone/gateway-token 0400 root root -
|
||||
f /etc/firezone/gateway-preset-env 0644 firezone firezone -
|
||||
20
rust/gateway/debian/postinst
Normal file
20
rust/gateway/debian/postinst
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script inspired by deb helper script from `cargo-deb`
|
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ]; then
|
||||
# In case this system is running systemd, we need to ensure that all
|
||||
# necessary users are created before starting.
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemd-sysusers firezone-gateway.conf >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
# Generate a deterministic ID based of `/etc/machine-id`
|
||||
FIREZONE_ID=$(systemd-id128 --app-specific=753b38f9f96947ef8083802d5909a372 machine-id)
|
||||
|
||||
# We fully overwrite the `gateway-present-env` file on every run.
|
||||
# Maintainer scripts need to be deterministic and that seems to be the easiest way.
|
||||
printf "# This file is managed by maintainer scripts. Do not touch!\n# Use \`etc/firezone/gateway-env\` for custom configuration.\n" >/etc/firezone/gateway-preset-env
|
||||
printf "FIREZONE_ID=%s\n" "${FIREZONE_ID}" >>/etc/firezone/gateway-preset-env
|
||||
Reference in New Issue
Block a user