mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-10 23:00:39 +02:00
chore: remove firezone again (waiting for upstream PR)
This commit is contained in:
parent
a3f74254e7
commit
2406dddd8e
21 changed files with 0 additions and 4208 deletions
|
@ -8,9 +8,6 @@
|
|||
./backups.nix
|
||||
./deterministic-ids.nix
|
||||
./distributed-config.nix
|
||||
./firezone-relay.nix
|
||||
./firezone-gateways.nix
|
||||
./firezone-server.nix
|
||||
./globals.nix
|
||||
./meta.nix
|
||||
./netbird-client.nix
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
concatMapAttrs
|
||||
flip
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.gateways = mkOption {
|
||||
description = ''
|
||||
A set of gateway clients to deploy on this machine. Each gateway can
|
||||
connect to exactly one firezone server.
|
||||
'';
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (gatewaysSubmod: {
|
||||
options = {
|
||||
package = mkPackageOption pkgs "firezone-gateway" { };
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = gatewaysSubmod.config._module.args.name;
|
||||
description = "The name of this gateway as shown in firezone";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.strMatching "^[a-zA-Z0-9_-]{1,32}$";
|
||||
default = "firezone-gw-${gatewaysSubmod.config._module.args.name}";
|
||||
description = "The DynamicUser name under which the gateway will run. Cannot exceed 32 characters.";
|
||||
};
|
||||
|
||||
interface = mkOption {
|
||||
type = types.strMatching "^[a-zA-Z0-9_-]{1,15}$";
|
||||
default = "tun-${gatewaysSubmod.config._module.args.name}";
|
||||
description = "The name of the TUN interface which will be created by this gateway";
|
||||
};
|
||||
|
||||
apiUrl = mkOption {
|
||||
type = types.str;
|
||||
example = "wss://firezone.example.com/api";
|
||||
description = ''
|
||||
The URL of your firezone server's API. This should be the same
|
||||
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
|
||||
but with `wss://` instead of `https://`.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/firezone-gateway-token";
|
||||
description = ''
|
||||
A file containing the firezone gateway token. Do not use a nix-store path here
|
||||
as it will make the token publicly readable!
|
||||
|
||||
This file will be passed via systemd credentials, it should only be accessible
|
||||
by the root user.
|
||||
'';
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "info";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
|
||||
enableTelemetry = mkEnableOption "telemetry";
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
systemd.services = flip concatMapAttrs config.services.firezone.gateways (
|
||||
gatewayName: gatewayCfg: {
|
||||
"firezone-gateway-${gatewayName}" = {
|
||||
description = "Gateway service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e gateway_id ]]; then
|
||||
uuidgen -r > gateway_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< gateway_id)
|
||||
fi
|
||||
|
||||
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
|
||||
exec ${getExe gatewayCfg.package}
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_API_URL = gatewayCfg.apiUrl;
|
||||
FIREZONE_NAME = gatewayCfg.name;
|
||||
FIREZONE_NO_TELEMETRY = boolToString gatewayCfg.enableTelemetry;
|
||||
FIREZONE_TUN_INTERFACE = gatewayCfg.interface;
|
||||
RUST_LOG = gatewayCfg.logLevel;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
|
||||
Type = "exec";
|
||||
DynamicUser = true;
|
||||
User = gatewayCfg.user;
|
||||
LoadCredential = [ "firezone-token:${gatewayCfg.tokenFile}" ];
|
||||
|
||||
DeviceAllow = "/dev/net/tun";
|
||||
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
|
||||
|
||||
StateDirectory = "firezone-gateways/${gatewayName}";
|
||||
WorkingDirectory = "/var/lib/firezone-gateways/${gatewayName}";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.relay;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.relay = {
|
||||
enable = mkEnableOption "the firezone relay server";
|
||||
package = mkPackageOption pkgs "firezone-relay" { };
|
||||
|
||||
publicIpv4 = mkOption {
|
||||
type = types.str;
|
||||
description = "The public ipv4 address of this relay";
|
||||
};
|
||||
|
||||
publicIpv6 = mkOption {
|
||||
type = types.str;
|
||||
description = "The public ipv6 address of this relay";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Opens up the main STUN port and the TURN allocation range.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3478;
|
||||
description = "The port to listen on for STUN messages";
|
||||
};
|
||||
|
||||
lowestPort = mkOption {
|
||||
type = types.port;
|
||||
default = 49152;
|
||||
description = "The lowest port to use in TURN allocation";
|
||||
};
|
||||
|
||||
highestPort = mkOption {
|
||||
type = types.port;
|
||||
default = 65535;
|
||||
description = "The highest port to use in TURN allocation";
|
||||
};
|
||||
|
||||
apiUrl = mkOption {
|
||||
type = types.str;
|
||||
example = "wss://firezone.example.com/api";
|
||||
description = ''
|
||||
The URL of your firezone server's API. This should be the same
|
||||
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
|
||||
but with `wss://` instead of `https://`.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/firezone-relay-token";
|
||||
description = ''
|
||||
A file containing the firezone relay token. Do not use a nix-store path here
|
||||
as it will make the token publicly readable!
|
||||
|
||||
This file will be passed via systemd credentials, it should only be accessible
|
||||
by the root user.
|
||||
'';
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "firezone_relay=info,firezone_tunnel=info,connlib_shared=info,tunnel_state=info,phoenix_channel=info,snownet=info,str0m=info,warn";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
|
||||
enableTelemetry = mkEnableOption "telemetry";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services."firezone-relay" = {
|
||||
description = "relay service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e relay_id ]]; then
|
||||
uuidgen -r > relay_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< relay_id)
|
||||
fi
|
||||
|
||||
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
|
||||
exec ${getExe cfg.package}
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_API_URL = cfg.apiUrl;
|
||||
FIREZONE_NAME = cfg.name;
|
||||
FIREZONE_TELEMETRY = boolToString cfg.enableTelemetry;
|
||||
|
||||
PUBLIC_IPV4_ADDRESS = cfg.publicIpv4;
|
||||
PUBLIC_IPV6_ADDRESS = cfg.publicIpv6;
|
||||
|
||||
LISTEN_PORT = toString cfg.port;
|
||||
LOWEST_PORT = toString cfg.lowestPort;
|
||||
HIGHEST_PORT = toString cfg.highestPort;
|
||||
|
||||
RUST_LOG = cfg.logLevel;
|
||||
LOG_FORMAT = "human";
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
|
||||
Type = "exec";
|
||||
DynamicUser = true;
|
||||
User = "firezone-relay";
|
||||
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
|
||||
|
||||
StateDirectory = "firezone-relay";
|
||||
WorkingDirectory = "/var/lib/firezone-relay";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
|
@ -1,948 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
attrNames
|
||||
boolToString
|
||||
concatLines
|
||||
concatLists
|
||||
concatMapAttrs
|
||||
concatStringsSep
|
||||
filterAttrs
|
||||
filterAttrsRecursive
|
||||
flip
|
||||
forEach
|
||||
getExe
|
||||
isBool
|
||||
mapAttrs
|
||||
mapAttrsToList
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkMerge
|
||||
mkOption
|
||||
mkPackageOption
|
||||
optionalAttrs
|
||||
recursiveUpdate
|
||||
subtractLists
|
||||
toUpper
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.server;
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
availableAuthAdapters = [
|
||||
"email"
|
||||
"openid_connect"
|
||||
"userpass"
|
||||
"token"
|
||||
"google_workspace"
|
||||
"microsoft_entra"
|
||||
"okta"
|
||||
"jumpcloud"
|
||||
];
|
||||
|
||||
# All non-secret environment variables or the given component
|
||||
collectEnvironment =
|
||||
component:
|
||||
mapAttrs (_: v: if isBool v then boolToString v else toString v) (
|
||||
cfg.settings // cfg.${component}.settings
|
||||
);
|
||||
|
||||
# All mandatory secrets which were not explicitly provided by the user will
|
||||
# have to be generated, if they do not yet exist.
|
||||
generateSecrets =
|
||||
let
|
||||
requiredSecrets = filterAttrs (_: v: v == null) cfg.settingsSecret;
|
||||
in
|
||||
''
|
||||
mkdir -p secrets
|
||||
chmod 700 secrets
|
||||
''
|
||||
+ concatLines (
|
||||
forEach (attrNames requiredSecrets) (secret: ''
|
||||
if [[ ! -e secrets/${secret} ]]; then
|
||||
echo "Generating ${secret}"
|
||||
# Some secrets like TOKENS_KEY_BASE require a value >=64 bytes.
|
||||
head -c 64 /dev/urandom | base64 -w 0 > secrets/${secret}
|
||||
chmod 600 secrets/${secret}
|
||||
fi
|
||||
'')
|
||||
);
|
||||
|
||||
# All secrets given in `cfg.settingsSecret` must be loaded from a file and
|
||||
# exported into the environment. Also exclude any variables that were
|
||||
# overwritten by the local component settings.
|
||||
loadSecretEnvironment =
|
||||
component:
|
||||
let
|
||||
relevantSecrets = subtractLists (attrNames cfg.${component}.settings) (
|
||||
attrNames cfg.settingsSecret
|
||||
);
|
||||
in
|
||||
concatLines (
|
||||
forEach relevantSecrets (
|
||||
secret:
|
||||
''export ${secret}=$(< ${
|
||||
if cfg.settingsSecret.${secret} == null then
|
||||
"secrets/${secret}"
|
||||
else
|
||||
"\"$CREDENTIALS_DIRECTORY/${secret}\""
|
||||
})''
|
||||
)
|
||||
);
|
||||
|
||||
provisionStateJson =
|
||||
let
|
||||
# Convert clientSecretFile options into the real counterpart
|
||||
augmentedAccounts = flip mapAttrs cfg.provision.accounts (
|
||||
accountName: account:
|
||||
account
|
||||
// {
|
||||
auth = flip mapAttrs account.auth (
|
||||
authName: auth:
|
||||
recursiveUpdate auth (
|
||||
optionalAttrs (auth.adapter_config.clientSecretFile != null) {
|
||||
adapter_config.client_secret = "{env:AUTH_CLIENT_SECRET_${toUpper accountName}_${toUpper authName}}";
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
in
|
||||
jsonFormat.generate "provision-state.json" {
|
||||
# Do not include any clientSecretFile attributes in the resulting json
|
||||
accounts = filterAttrsRecursive (k: _: k != "clientSecretFile") augmentedAccounts;
|
||||
};
|
||||
|
||||
commonServiceConfig = {
|
||||
AmbientCapablities = [ ];
|
||||
CapabilityBoundingSet = [ ];
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
|
||||
DynamicUser = true;
|
||||
User = "firezone";
|
||||
|
||||
Slice = "system-firezone.slice";
|
||||
StateDirectory = "firezone";
|
||||
WorkingDirectory = "/var/lib/firezone";
|
||||
|
||||
LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") (
|
||||
filterAttrs (_: v: v != null) cfg.settingsSecret
|
||||
);
|
||||
Type = "exec";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
};
|
||||
|
||||
componentOptions = component: {
|
||||
enable = mkEnableOption "the Firezone ${component} server";
|
||||
package = mkPackageOption pkgs "firezone-server-${component}" { };
|
||||
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Environment variables for this component of the Firezone server. For a
|
||||
list of available variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex).
|
||||
Some variables like `OUTBOUND_EMAIL_ADAPTER_OPTS` require json values
|
||||
for which you can use `VAR = builtins.toJSON { /* ... */ }`.
|
||||
|
||||
This component will automatically inherit all variables defined via
|
||||
{option}`services.firezone.server.settings` and
|
||||
{option}`services.firezone.server.settingsSecret`, but which can be
|
||||
overwritten by this option.
|
||||
'';
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf (
|
||||
types.oneOf [
|
||||
types.bool
|
||||
types.float
|
||||
types.int
|
||||
types.str
|
||||
types.path
|
||||
types.package
|
||||
]
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services.firezone.server = {
|
||||
enable = mkEnableOption "all Firezone components";
|
||||
enableLocalDB = mkEnableOption "a local postgresql database for Firezone";
|
||||
nginx.enable = mkEnableOption "nginx virtualhost definition";
|
||||
|
||||
openClusterFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Opens up the erlang distribution port of all enabled components to
|
||||
allow reaching the server cluster from the internet. You only need to
|
||||
set this if you are actually distributing your cluster across multiple
|
||||
machines.
|
||||
'';
|
||||
};
|
||||
|
||||
clusterHosts = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [
|
||||
"api@localhost.localdomain"
|
||||
"web@localhost.localdomain"
|
||||
"domain@localhost.localdomain"
|
||||
];
|
||||
description = ''
|
||||
A list of components and their hosts that are part of this cluster. For
|
||||
a single-machine setup, the default value will be sufficient. This
|
||||
value will automatically set `ERLANG_CLUSTER_ADAPTER_CONFIG`.
|
||||
|
||||
The format is `<COMPONENT_NAME>@<HOSTNAME>`.
|
||||
'';
|
||||
};
|
||||
|
||||
settingsSecret = mkOption {
|
||||
default = { };
|
||||
description = ''
|
||||
This is a convenience option which allows you to set secret values for
|
||||
environment variables by specifying a file which will contain the value
|
||||
at runtime. Before starting the server, the content of each file will
|
||||
be loaded into the respective environment variable.
|
||||
|
||||
Otherwise, this option is equivalent to
|
||||
{option}`services.firezone.server.settings`. Refer to the settings
|
||||
option for more information regarding the actual variables and how
|
||||
filtering rules are applied for each component.
|
||||
'';
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf types.path;
|
||||
options = {
|
||||
RELEASE_COOKIE = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique secret identifier for the Erlang
|
||||
cluster. All Firezone components in your cluster must use the
|
||||
same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
TOKENS_KEY_BASE = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`TOKENS_KEY_BASE`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
SECRET_KEY_BASE = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`SECRET_KEY_BASE`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
TOKENS_SALT = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`TOKENS_SALT`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
LIVE_VIEW_SIGNING_SALT = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`LIVE_VIEW_SIGNING_SALT`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
COOKIE_SIGNING_SALT = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`COOKIE_SIGNING_SALT`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
|
||||
COOKIE_ENCRYPTION_SALT = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a unique base64 encoded secret for the
|
||||
`COOKIE_ENCRYPTION_SALT`. All Firezone components in your cluster must
|
||||
use the same value.
|
||||
|
||||
If this is `null`, a shared value will automatically be generated
|
||||
on startup and used for all components on this machine. You do
|
||||
not need to set this except when you spread your cluster over
|
||||
multiple hosts.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Environment variables for the Firezone server. For a list of available
|
||||
variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex).
|
||||
Some variables like `OUTBOUND_EMAIL_ADAPTER_OPTS` require json values
|
||||
for which you can use `VAR = builtins.toJSON { /* ... */ }`.
|
||||
|
||||
Each component has an additional `settings` option which allows you to
|
||||
override specific variables passed to that component.
|
||||
'';
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf (
|
||||
types.oneOf [
|
||||
types.bool
|
||||
types.float
|
||||
types.int
|
||||
types.str
|
||||
types.path
|
||||
types.package
|
||||
]
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
smtp = {
|
||||
configureManually = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Outbound email configuration is mandatory for Firezone and supports
|
||||
many different delivery adapters. Yet, most users will only need an
|
||||
SMTP relay to send emails, so this configuration enforced by default.
|
||||
|
||||
If you want to utilize an alternative way to send emails (e.g. via a
|
||||
supportd API-based service), enable this option and define
|
||||
`OUTBOUND_EMAIL_FROM`, `OUTBOUND_EMAIL_ADAPTER` and
|
||||
`OUTBOUND_EMAIL_ADAPTER_OPTS` manually via
|
||||
{option}`services.firezone.server.settings` and/or
|
||||
{option}`services.firezone.server.settingsSecret`.
|
||||
|
||||
The Firezone documentation holds [a list of supported Swoosh adapters](https://github.com/firezone/firezone/blob/main/website/src/app/docs/reference/env-vars/readme.mdx#outbound-emails).
|
||||
'';
|
||||
};
|
||||
|
||||
from = mkOption {
|
||||
type = types.str;
|
||||
example = "firezone@example.com";
|
||||
description = "Outbound SMTP FROM address";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
example = "mail.example.com";
|
||||
description = "Outbound SMTP host";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
example = 465;
|
||||
description = "Outbound SMTP port";
|
||||
};
|
||||
|
||||
implicitTls = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to use implicit TLS instead of STARTTLS (usually port 465)";
|
||||
};
|
||||
|
||||
username = mkOption {
|
||||
type = types.str;
|
||||
example = "firezone@example.com";
|
||||
description = "Username to authenticate against the SMTP relay";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/smtp-password";
|
||||
description = "File containing the password for the given username. Beware that a file in the nix store will be world readable.";
|
||||
};
|
||||
};
|
||||
|
||||
domain = componentOptions "domain";
|
||||
|
||||
web = componentOptions "web" // {
|
||||
externalUrl = mkOption {
|
||||
type = types.strMatching "^https://.+/$";
|
||||
example = "https://firezone.example.com/";
|
||||
description = ''
|
||||
The external URL under which you will serve the web interface. You
|
||||
need to setup a reverse proxy for TLS termination, either with
|
||||
{option}`services.firezone.server.nginx.enable` or manually.
|
||||
'';
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "The address to listen on";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8080;
|
||||
description = "The port under which the web interface will be served locally";
|
||||
};
|
||||
|
||||
trustedProxies = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "A list of trusted proxies";
|
||||
};
|
||||
};
|
||||
|
||||
api = componentOptions "api" // {
|
||||
externalUrl = mkOption {
|
||||
type = types.strMatching "^https://.+/$";
|
||||
example = "https://firezone.example.com/api/";
|
||||
description = ''
|
||||
The external URL under which you will serve the api. You need to
|
||||
setup a reverse proxy for TLS termination, either with
|
||||
{option}`services.firezone.server.nginx.enable` or manually.
|
||||
'';
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "The address to listen on";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8081;
|
||||
description = "The port under which the api will be served locally";
|
||||
};
|
||||
|
||||
trustedProxies = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "A list of trusted proxies";
|
||||
};
|
||||
};
|
||||
|
||||
provision = {
|
||||
enable = mkEnableOption "provisioning of the Firezone domain server";
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
freeformType = jsonFormat.type;
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The account name";
|
||||
example = "My Organization";
|
||||
};
|
||||
|
||||
features =
|
||||
let
|
||||
mkFeatureOption =
|
||||
name: default:
|
||||
mkOption {
|
||||
type = types.bool;
|
||||
inherit default;
|
||||
description = "Whether to enable the `${name}` feature for this account.";
|
||||
};
|
||||
in
|
||||
{
|
||||
flow_activities = mkFeatureOption "flow_activities" true;
|
||||
policy_conditions = mkFeatureOption "policy_conditions" true;
|
||||
multi_site_resources = mkFeatureOption "multi_site_resources" true;
|
||||
traffic_filters = mkFeatureOption "traffic_filters" true;
|
||||
self_hosted_relays = mkFeatureOption "self_hosted_relays" true;
|
||||
idp_sync = mkFeatureOption "idp_sync" true;
|
||||
rest_api = mkFeatureOption "rest_api" true;
|
||||
internet_resource = mkFeatureOption "internet_resource" true;
|
||||
};
|
||||
|
||||
actors = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
freeformType = jsonFormat.type;
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.enum [
|
||||
"account_admin_user"
|
||||
"account_user"
|
||||
"service_account"
|
||||
"api_client"
|
||||
];
|
||||
description = "The account type";
|
||||
};
|
||||
|
||||
email = mkOption {
|
||||
type = types.str;
|
||||
description = "The email address used to authenticate as this account";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
admin = {
|
||||
type = "account_admin_user";
|
||||
email = "admin@myorg.example.com";
|
||||
};
|
||||
};
|
||||
description = "All actors (users) to provision.";
|
||||
};
|
||||
|
||||
auth = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
freeformType = jsonFormat.type;
|
||||
options = {
|
||||
adapter = mkOption {
|
||||
type = types.enum availableAuthAdapters;
|
||||
description = "The auth adapter type";
|
||||
};
|
||||
|
||||
adapter_config.clientSecretFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
A file containing a the client secret for an openid_connect adapter.
|
||||
You only need to set this if this is an openid_connect provider.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
myoidcprovider = {
|
||||
adapter = "openid_connect";
|
||||
adapter_config = {
|
||||
client_id = "clientid";
|
||||
clientSecretFile = "/run/secrets/oidc-client-secret";
|
||||
response_type = "code";
|
||||
scope = "openid email name";
|
||||
discorvery_document_uri = "https://auth.example.com/.well-known/openid-configuration";
|
||||
};
|
||||
};
|
||||
};
|
||||
description = "All authentication providers to provision.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
main = {
|
||||
name = "My Account / Organization";
|
||||
metadata.stripe.billing_email = "org@myorg.example.com";
|
||||
features.rest_api = false;
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
All accounts to provision. The key specified here will become the
|
||||
account slug. By using `"{file:/path/to/file}"` as a string value
|
||||
anywhere in these settings, the provisioning script will replace that
|
||||
value with the content of the given file at runtime.
|
||||
|
||||
Please refer to the [Firezone source code](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/accounts/account.ex)
|
||||
for all available properties.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
{
|
||||
assertions =
|
||||
[
|
||||
{
|
||||
assertion = cfg.provision.enable -> cfg.domain.enable;
|
||||
message = "Provisioning must be done on a machine running the firezone domain server";
|
||||
}
|
||||
]
|
||||
++ concatLists (
|
||||
flip mapAttrsToList cfg.provision.accounts (
|
||||
accountName: accountCfg:
|
||||
[
|
||||
{
|
||||
assertion = (builtins.match "^[[:lower:]_-]+$" accountName) != null;
|
||||
message = "An account name must contain only lowercase characters and underscores, as it will be used as the URL slug for this account.";
|
||||
}
|
||||
]
|
||||
++ flip mapAttrsToList accountCfg.auth (
|
||||
authName: _: {
|
||||
assertion = (builtins.match "^[[:alnum:]_-]+$" authName) != null;
|
||||
message = "An authentication provider name must contain only letters, numbers, underscores or dashes.";
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
# Enable all components if the main server is enabled
|
||||
(mkIf cfg.enable {
|
||||
services.firezone.server.domain.enable = true;
|
||||
services.firezone.server.web.enable = true;
|
||||
services.firezone.server.api.enable = true;
|
||||
})
|
||||
# Create (and configure) a local database if desired
|
||||
(mkIf cfg.enableLocalDB {
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "firezone";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
ensureDatabases = [ "firezone" ];
|
||||
};
|
||||
|
||||
services.firezone.server.settings = {
|
||||
DATABASE_SOCKET_DIR = "/run/postgresql";
|
||||
DATABASE_PORT = "5432";
|
||||
DATABASE_NAME = "firezone";
|
||||
DATABASE_USER = "firezone";
|
||||
};
|
||||
})
|
||||
# Create a local nginx reverse proxy
|
||||
(mkIf cfg.nginx.enable {
|
||||
services.nginx = mkMerge [
|
||||
{
|
||||
enable = true;
|
||||
}
|
||||
(
|
||||
let
|
||||
urlComponents = builtins.elemAt (builtins.split "https://([^/]*)(/?.*)" cfg.web.externalUrl) 1;
|
||||
domain = builtins.elemAt urlComponents 0;
|
||||
location = builtins.elemAt urlComponents 1;
|
||||
in
|
||||
{
|
||||
virtualHosts.${domain} = {
|
||||
forceSSL = mkDefault true;
|
||||
locations.${location} = {
|
||||
# The trailing slash is important to strip the location prefix from the request
|
||||
proxyPass = "http://${cfg.web.address}:${toString cfg.web.port}/";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
(
|
||||
let
|
||||
urlComponents = builtins.elemAt (builtins.split "https://([^/]*)(/?.*)" cfg.api.externalUrl) 1;
|
||||
domain = builtins.elemAt urlComponents 0;
|
||||
location = builtins.elemAt urlComponents 1;
|
||||
in
|
||||
{
|
||||
virtualHosts.${domain} = {
|
||||
forceSSL = mkDefault true;
|
||||
locations.${location} = {
|
||||
# The trailing slash is important to strip the location prefix from the request
|
||||
proxyPass = "http://${cfg.api.address}:${toString cfg.api.port}/";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
})
|
||||
# Specify sensible defaults
|
||||
{
|
||||
services.firezone.server = {
|
||||
settings = {
|
||||
LOG_LEVEL = mkDefault "info";
|
||||
RELEASE_HOSTNAME = mkDefault "localhost.localdomain";
|
||||
|
||||
ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd";
|
||||
ERLANG_CLUSTER_ADAPTER_CONFIG = mkDefault (
|
||||
builtins.toJSON {
|
||||
hosts = cfg.clusterHosts;
|
||||
}
|
||||
);
|
||||
|
||||
TZDATA_DIR = mkDefault "/var/lib/firezone/tzdata";
|
||||
TELEMETRY_ENABLED = mkDefault false;
|
||||
|
||||
# By default this will open nproc * 2 connections for each component,
|
||||
# which can exceeds the (default) maximum of 100 connections for
|
||||
# postgresql on a 12 core +SMT machine. 16 connections will be
|
||||
# sufficient for small to medium deployments
|
||||
DATABASE_POOL_SIZE = "16";
|
||||
|
||||
AUTH_PROVIDER_ADAPTERS = mkDefault (concatStringsSep "," availableAuthAdapters);
|
||||
|
||||
FEATURE_FLOW_ACTIVITIES_ENABLED = mkDefault true;
|
||||
FEATURE_POLICY_CONDITIONS_ENABLED = mkDefault true;
|
||||
FEATURE_MULTI_SITE_RESOURCES_ENABLED = mkDefault true;
|
||||
FEATURE_SELF_HOSTED_RELAYS_ENABLED = mkDefault true;
|
||||
FEATURE_IDP_SYNC_ENABLED = mkDefault true;
|
||||
FEATURE_REST_API_ENABLED = mkDefault true;
|
||||
FEATURE_INTERNET_RESOURCE_ENABLED = mkDefault true;
|
||||
FEATURE_TRAFFIC_FILTERS_ENABLED = mkDefault true;
|
||||
FEATURE_SIGN_UP_ENABLED = mkDefault true;
|
||||
};
|
||||
|
||||
domain.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9000;
|
||||
HEALTHZ_PORT = mkDefault 4000;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault true;
|
||||
};
|
||||
|
||||
web.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9001;
|
||||
HEALTHZ_PORT = mkDefault 4001;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||
|
||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address;
|
||||
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies);
|
||||
PHOENIX_HTTP_WEB_PORT = mkDefault cfg.web.port;
|
||||
PHOENIX_HTTP_API_PORT = mkDefault cfg.api.port;
|
||||
PHOENIX_SECURE_COOKIES = mkDefault true; # enforce HTTPS on cookies
|
||||
WEB_EXTERNAL_URL = mkDefault cfg.web.externalUrl;
|
||||
API_EXTERNAL_URL = mkDefault cfg.api.externalUrl;
|
||||
};
|
||||
|
||||
api.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9002;
|
||||
HEALTHZ_PORT = mkDefault 4002;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||
|
||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address;
|
||||
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.api.trustedProxies);
|
||||
PHOENIX_HTTP_WEB_PORT = mkDefault cfg.web.port;
|
||||
PHOENIX_HTTP_API_PORT = mkDefault cfg.api.port;
|
||||
PHOENIX_SECURE_COOKIES = mkDefault true; # enforce HTTPS on cookies
|
||||
WEB_EXTERNAL_URL = mkDefault cfg.web.externalUrl;
|
||||
API_EXTERNAL_URL = mkDefault cfg.api.externalUrl;
|
||||
};
|
||||
};
|
||||
}
|
||||
(mkIf (!cfg.smtp.configureManually) {
|
||||
services.firezone.server.settings = {
|
||||
OUTBOUND_EMAIL_ADAPTER = "Elixir.Swoosh.Adapters.Mua";
|
||||
OUTBOUND_EMAIL_ADAPTER_OPTS = builtins.toJSON { };
|
||||
OUTBOUND_EMAIL_FROM = cfg.smtp.from;
|
||||
OUTBOUND_EMAIL_SMTP_HOST = cfg.smtp.host;
|
||||
OUTBOUND_EMAIL_SMTP_PORT = toString cfg.smtp.port;
|
||||
OUTBOUND_EMAIL_SMTP_PROTOCOL = if cfg.smtp.implicitTls then "ssl" else "tcp";
|
||||
OUTBOUND_EMAIL_SMTP_USERNAME = cfg.smtp.username;
|
||||
};
|
||||
services.firezone.server.settingsSecret = {
|
||||
OUTBOUND_EMAIL_SMTP_PASSWORD = cfg.smtp.passwordFile;
|
||||
};
|
||||
})
|
||||
(mkIf cfg.provision.enable {
|
||||
# Load client secrets from authentication providers
|
||||
services.firezone.server.settingsSecret = flip concatMapAttrs cfg.provision.accounts (
|
||||
accountName: accountCfg:
|
||||
flip concatMapAttrs accountCfg.auth (
|
||||
authName: authCfg:
|
||||
optionalAttrs (authCfg.adapter_config.clientSecretFile != null) {
|
||||
"AUTH_CLIENT_SECRET_${toUpper accountName}_${toUpper authName}" =
|
||||
authCfg.adapter_config.clientSecretFile;
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
(mkIf (cfg.openClusterFirewall && cfg.domain.enable) {
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
cfg.domain.settings.ERLANG_DISTRIBUTION_PORT
|
||||
];
|
||||
})
|
||||
(mkIf (cfg.openClusterFirewall && cfg.web.enable) {
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
cfg.web.settings.ERLANG_DISTRIBUTION_PORT
|
||||
];
|
||||
})
|
||||
(mkIf (cfg.openClusterFirewall && cfg.api.enable) {
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
cfg.api.settings.ERLANG_DISTRIBUTION_PORT
|
||||
];
|
||||
})
|
||||
(mkIf (cfg.domain.enable || cfg.web.enable || cfg.api.enable) {
|
||||
systemd.slices.system-firezone = {
|
||||
description = "Firezone Slice";
|
||||
};
|
||||
|
||||
systemd.targets.firezone = {
|
||||
description = "Common target for all Firezone services.";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
systemd.services.firezone-initialize = {
|
||||
description = "Backend initialization service for the Firezone zero-trust access platform";
|
||||
|
||||
after = mkIf cfg.enableLocalDB [ "postgresql.service" ];
|
||||
requires = mkIf cfg.enableLocalDB [ "postgresql.service" ];
|
||||
wantedBy = [ "firezone.target" ];
|
||||
partOf = [ "firezone.target" ];
|
||||
|
||||
script = ''
|
||||
mkdir -p "$TZDATA_DIR"
|
||||
|
||||
# Generate and load secrets
|
||||
${generateSecrets}
|
||||
${loadSecretEnvironment "domain"}
|
||||
|
||||
echo "Running migrations"
|
||||
${getExe cfg.domain.package} eval Domain.Release.migrate
|
||||
'';
|
||||
|
||||
# We use the domain environment to be able to run migrations
|
||||
environment = collectEnvironment "domain";
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.firezone-server-domain = mkIf cfg.domain.enable {
|
||||
description = "Backend domain server for the Firezone zero-trust access platform";
|
||||
after = [ "firezone-initialize.service" ];
|
||||
bindsTo = [ "firezone-initialize.service" ];
|
||||
wantedBy = [ "firezone.target" ];
|
||||
partOf = [ "firezone.target" ];
|
||||
|
||||
script = ''
|
||||
${loadSecretEnvironment "domain"}
|
||||
exec ${getExe cfg.domain.package} start;
|
||||
'';
|
||||
|
||||
path = [ pkgs.curl ];
|
||||
postStart = mkIf cfg.provision.enable ''
|
||||
${loadSecretEnvironment "domain"}
|
||||
|
||||
# Wait for the firezone server to come online
|
||||
count=0
|
||||
while [[ "$(curl -s "http://localhost:${toString cfg.domain.settings.HEALTHZ_PORT}" 2>/dev/null || echo)" != '{"status":"ok"}' ]]
|
||||
do
|
||||
sleep 1
|
||||
if [[ "$count" -eq 30 ]]; then
|
||||
echo "Tried for at least 30 seconds, giving up..."
|
||||
exit 1
|
||||
fi
|
||||
count=$((count++))
|
||||
done
|
||||
|
||||
sleep 1 # Wait for server to fully come up. Not ideal to use sleep, but at least it works.
|
||||
ln -sTf ${provisionStateJson} provision-state.json
|
||||
${getExe cfg.domain.package} rpc 'Code.eval_file("${./provision.exs}")'
|
||||
'';
|
||||
|
||||
environment = collectEnvironment "domain";
|
||||
serviceConfig = commonServiceConfig;
|
||||
};
|
||||
|
||||
systemd.services.firezone-server-web = mkIf cfg.web.enable {
|
||||
description = "Backend web server for the Firezone zero-trust access platform";
|
||||
after = [ "firezone-initialize.service" ];
|
||||
bindsTo = [ "firezone-initialize.service" ];
|
||||
wantedBy = [ "firezone.target" ];
|
||||
partOf = [ "firezone.target" ];
|
||||
|
||||
script = ''
|
||||
${loadSecretEnvironment "web"}
|
||||
exec ${getExe cfg.web.package} start;
|
||||
'';
|
||||
|
||||
environment = collectEnvironment "web";
|
||||
serviceConfig = commonServiceConfig;
|
||||
};
|
||||
|
||||
systemd.services.firezone-server-api = mkIf cfg.api.enable {
|
||||
description = "Backend api server for the Firezone zero-trust access platform";
|
||||
after = [ "firezone-initialize.service" ];
|
||||
bindsTo = [ "firezone-initialize.service" ];
|
||||
wantedBy = [ "firezone.target" ];
|
||||
partOf = [ "firezone.target" ];
|
||||
|
||||
script = ''
|
||||
${loadSecretEnvironment "api"}
|
||||
exec ${getExe cfg.api.package} start;
|
||||
'';
|
||||
|
||||
environment = collectEnvironment "api";
|
||||
serviceConfig = commonServiceConfig;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
defmodule Provision do
|
||||
alias Domain.{Repo, Accounts, Auth, Actors}
|
||||
require Logger
|
||||
|
||||
defp resolve_references(value) when is_map(value) do
|
||||
Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
|
||||
end
|
||||
|
||||
defp resolve_references(value) when is_list(value) do
|
||||
Enum.map(value, &resolve_references/1)
|
||||
end
|
||||
|
||||
defp resolve_references(value) when is_binary(value) do
|
||||
Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
|
||||
System.get_env(var) || raise "Environment variable #{var} not set"
|
||||
end)
|
||||
end
|
||||
|
||||
defp resolve_references(value), do: value
|
||||
|
||||
defp atomize_keys(map) when is_map(map) do
|
||||
Enum.into(map, %{}, fn {k, v} ->
|
||||
{
|
||||
if(is_binary(k), do: String.to_atom(k), else: k),
|
||||
if(is_map(v), do: atomize_keys(v), else: v)
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
def provision() do
|
||||
IO.inspect("Starting provisioning", label: "INFO")
|
||||
json_file = "provision-state.json"
|
||||
{:ok, raw_json} = File.read(json_file)
|
||||
{:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
|
||||
accounts = resolve_references(accounts)
|
||||
|
||||
multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
|
||||
account_attrs = atomize_keys(%{
|
||||
name: account_data["name"],
|
||||
slug: slug,
|
||||
features: Map.get(account_data, "features", %{}),
|
||||
metadata: Map.get(account_data, "metadata", %{}),
|
||||
limits: Map.get(account_data, "limits", %{}),
|
||||
})
|
||||
|
||||
multi = multi
|
||||
|> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
|
||||
case Accounts.fetch_account_by_id_or_slug(slug) do
|
||||
{:ok, acc} ->
|
||||
IO.inspect("Updating existing account #{slug}", label: "INFO")
|
||||
updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
|
||||
{:ok, {:existing, updated_acc}}
|
||||
_ ->
|
||||
IO.inspect("Creating new account #{slug}", label: "INFO")
|
||||
{:ok, account} = Accounts.create_account(account_attrs)
|
||||
{:ok, {:new, account}}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
|
||||
case Map.get(changes, {:account, slug}) do
|
||||
{:new, account} ->
|
||||
IO.inspect("Creating Everyone group for new account", label: "INFO")
|
||||
Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]})
|
||||
{:existing, _account} ->
|
||||
{:ok, :skipped}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:provider, slug}, fn _repo, changes ->
|
||||
case Map.get(changes, {:account, slug}) do
|
||||
{:new, account} ->
|
||||
IO.inspect("Creating default email provider for new account", label: "INFO")
|
||||
Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
|
||||
{:existing, account} ->
|
||||
Auth.Provider.Query.not_disabled()
|
||||
|> Auth.Provider.Query.by_adapter(:email)
|
||||
|> Auth.Provider.Query.by_account_id(account.id)
|
||||
|> Repo.fetch(Auth.Provider.Query, [])
|
||||
end
|
||||
end)
|
||||
|
||||
multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {name, actor_data}, multi ->
|
||||
actor_attrs = atomize_keys(%{
|
||||
name: name,
|
||||
type: String.to_atom(actor_data["type"]),
|
||||
})
|
||||
|
||||
Ecto.Multi.run(multi, {:actor, slug, name}, fn repo, changes ->
|
||||
{_, account} = changes[{:account, slug}]
|
||||
case Repo.get_by(Actors.Actor, account_id: account.id, name: name) do
|
||||
nil ->
|
||||
IO.inspect("Creating new actor #{name}", label: "INFO")
|
||||
{:ok, actor} = Actors.create_actor(account, actor_attrs)
|
||||
{:ok, {:new, actor}}
|
||||
act ->
|
||||
IO.inspect("Updating existing actor #{name}", label: "INFO")
|
||||
updated_act = act |> Ecto.Changeset.change(actor_attrs) |> repo.update!()
|
||||
{:ok, {:existing, updated_act}}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:actor_identity, slug, name}, fn repo, changes ->
|
||||
email_provider = changes[{:provider, slug}]
|
||||
case Map.get(changes, {:actor, slug, name}) do
|
||||
{:new, actor} ->
|
||||
IO.inspect("Creating actor email identity", label: "INFO")
|
||||
Auth.create_identity(actor, email_provider, %{
|
||||
provider_identifier: actor_data["email"],
|
||||
provider_identifier_confirmation: actor_data["email"]
|
||||
})
|
||||
{:existing, actor} ->
|
||||
IO.inspect("Updating actor email identity", label: "INFO")
|
||||
{:ok, identity} = Auth.Identity.Query.not_deleted()
|
||||
|> Auth.Identity.Query.by_actor_id(actor.id)
|
||||
|> Auth.Identity.Query.by_provider_id(email_provider.id)
|
||||
|> Repo.fetch(Auth.Identity.Query, [])
|
||||
|
||||
{:ok, identity |> Ecto.Changeset.change(%{
|
||||
provider_identifier: actor_data["email"],
|
||||
}) |> repo.update!()}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {name, provider_data}, multi ->
|
||||
provider_attrs = %{
|
||||
name: name,
|
||||
adapter: String.to_atom(provider_data["adapter"]),
|
||||
adapter_config: provider_data["adapter_config"],
|
||||
}
|
||||
|
||||
Ecto.Multi.run(multi, {:provider, slug, name}, fn repo, changes ->
|
||||
{_, account} = changes[{:account, slug}]
|
||||
case Repo.get_by(Auth.Provider, account_id: account.id, name: name) do
|
||||
nil ->
|
||||
IO.inspect("Creating new provider #{name}", label: "INFO")
|
||||
Auth.create_provider(account, provider_attrs)
|
||||
existing ->
|
||||
IO.inspect("Updating existing provider #{name}", label: "INFO")
|
||||
{:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
multi
|
||||
end)
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, _result} ->
|
||||
Logger.info("Provisioning completed successfully.")
|
||||
{:error, step, reason, _changes} ->
|
||||
Logger.error("Provisioning failed at step #{inspect(step)}: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Provision.provision()
|
Loading…
Add table
Add a link
Reference in a new issue