mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-10 23:00:39 +02:00
chore: more firezone
This commit is contained in:
parent
eeb248b582
commit
bbb76ae5ec
4 changed files with 528 additions and 66 deletions
|
@ -9,7 +9,12 @@ let
|
||||||
attrNames
|
attrNames
|
||||||
boolToString
|
boolToString
|
||||||
concatLines
|
concatLines
|
||||||
|
concatLists
|
||||||
|
concatMapAttrs
|
||||||
|
concatStringsSep
|
||||||
filterAttrs
|
filterAttrs
|
||||||
|
filterAttrsRecursive
|
||||||
|
flip
|
||||||
forEach
|
forEach
|
||||||
getExe
|
getExe
|
||||||
isBool
|
isBool
|
||||||
|
@ -20,11 +25,26 @@ let
|
||||||
mkIf
|
mkIf
|
||||||
mkMerge
|
mkMerge
|
||||||
mkOption
|
mkOption
|
||||||
|
mkPackageOption
|
||||||
|
optionalAttrs
|
||||||
|
recursiveUpdate
|
||||||
subtractLists
|
subtractLists
|
||||||
|
toUpper
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
|
|
||||||
cfg = config.services.firezone.server;
|
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
|
# All non-secret environment variables or the given component
|
||||||
collectEnvironment =
|
collectEnvironment =
|
||||||
|
@ -75,40 +95,60 @@ let
|
||||||
'')
|
'')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 = {
|
commonServiceConfig = {
|
||||||
# AmbientCapablities = "CAP_NET_ADMIN";
|
AmbientCapablities = [ ];
|
||||||
# CapabilityBoundingSet = "CAP_CHOWN CAP_NET_ADMIN";
|
CapabilityBoundingSet = [ ];
|
||||||
# DeviceAllow = "/dev/net/tun";
|
LockPersonality = "true";
|
||||||
# LockPersonality = "true";
|
MemoryDenyWriteExecute = "true";
|
||||||
# LogsDirectory = "dev.firezone.client";
|
NoNewPrivileges = "true";
|
||||||
# LogsDirectoryMode = "755";
|
PrivateMounts = "true";
|
||||||
# MemoryDenyWriteExecute = "true";
|
PrivateTmp = "true";
|
||||||
# NoNewPrivileges = "true";
|
PrivateUsers = "false";
|
||||||
# PrivateMounts = "true";
|
ProcSubset = "pid";
|
||||||
# PrivateTmp = "true";
|
ProtectClock = "true";
|
||||||
# PrivateUsers = "false";
|
ProtectControlGroups = "true";
|
||||||
# ProcSubset = "pid";
|
ProtectHome = "true";
|
||||||
# ProtectClock = "true";
|
ProtectHostname = "true";
|
||||||
# ProtectControlGroups = "true";
|
ProtectKernelLogs = "true";
|
||||||
# ProtectHome = "true";
|
ProtectKernelModules = "true";
|
||||||
# ProtectHostname = "true";
|
ProtectKernelTunables = "true";
|
||||||
# ProtectKernelLogs = "true";
|
ProtectProc = "invisible";
|
||||||
# ProtectKernelModules = "true";
|
ProtectSystem = "strict";
|
||||||
# ProtectKernelTunables = "true";
|
RestrictAddressFamilies = [
|
||||||
# ProtectProc = "invisible";
|
"AF_INET"
|
||||||
# ProtectSystem = "strict";
|
"AF_INET6"
|
||||||
# RestrictAddressFamilies = [
|
"AF_NETLINK"
|
||||||
# "AF_INET"
|
"AF_UNIX"
|
||||||
# "AF_INET6"
|
];
|
||||||
# "AF_NETLINK"
|
RestrictNamespaces = "true";
|
||||||
# "AF_UNIX"
|
RestrictRealtime = "true";
|
||||||
# ];
|
RestrictSUIDSGID = "true";
|
||||||
# RestrictNamespaces = "true";
|
SystemCallArchitectures = "native";
|
||||||
# RestrictRealtime = "true";
|
SystemCallFilter = "@system-service";
|
||||||
# RestrictSUIDSGID = "true";
|
UMask = "077";
|
||||||
# SystemCallArchitectures = "native";
|
|
||||||
# SystemCallFilter = "@aio @basic-io @file-system @io-event @ipc @network-io @signal @system-service";
|
|
||||||
# UMask = "077";
|
|
||||||
|
|
||||||
DynamicUser = true;
|
DynamicUser = true;
|
||||||
User = "firezone";
|
User = "firezone";
|
||||||
|
@ -117,6 +157,7 @@ let
|
||||||
StateDirectory = "firezone";
|
StateDirectory = "firezone";
|
||||||
WorkingDirectory = "/var/lib/firezone";
|
WorkingDirectory = "/var/lib/firezone";
|
||||||
|
|
||||||
|
Type = "exec";
|
||||||
LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") (
|
LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") (
|
||||||
filterAttrs (_: v: v != null) cfg.settingsSecret
|
filterAttrs (_: v: v != null) cfg.settingsSecret
|
||||||
);
|
);
|
||||||
|
@ -124,10 +165,9 @@ let
|
||||||
|
|
||||||
componentOptions = component: {
|
componentOptions = component: {
|
||||||
enable = mkEnableOption "the Firezone ${component} server";
|
enable = mkEnableOption "the Firezone ${component} server";
|
||||||
# TODO: single package plus web and api passthrough.
|
package = mkPackageOption pkgs "firezone-server-${component}" { };
|
||||||
# package = mkPackageOption pkgs "firezone-server" { };
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
settings = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Environment variables for this component of the Firezone server. For a
|
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).
|
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).
|
||||||
|
@ -140,7 +180,7 @@ let
|
||||||
overwritten by this option.
|
overwritten by this option.
|
||||||
'';
|
'';
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = types.submodule {
|
||||||
freeformType = types.attrsOf (
|
freeformType = types.attrsOf (
|
||||||
types.oneOf [
|
types.oneOf [
|
||||||
types.bool
|
types.bool
|
||||||
|
@ -214,7 +254,7 @@ in
|
||||||
option for more information regarding the actual variables and how
|
option for more information regarding the actual variables and how
|
||||||
filtering rules are applied for each component.
|
filtering rules are applied for each component.
|
||||||
'';
|
'';
|
||||||
type = lib.types.submodule {
|
type = types.submodule {
|
||||||
freeformType = types.attrsOf types.path;
|
freeformType = types.attrsOf types.path;
|
||||||
options = {
|
options = {
|
||||||
RELEASE_COOKIE = mkOption {
|
RELEASE_COOKIE = mkOption {
|
||||||
|
@ -325,7 +365,7 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = lib.mkOption {
|
settings = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Environment variables for the Firezone server. For a list of available
|
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).
|
variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex).
|
||||||
|
@ -336,7 +376,7 @@ in
|
||||||
override specific variables passed to that component.
|
override specific variables passed to that component.
|
||||||
'';
|
'';
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = types.submodule {
|
||||||
freeformType = types.attrsOf (
|
freeformType = types.attrsOf (
|
||||||
types.oneOf [
|
types.oneOf [
|
||||||
types.bool
|
types.bool
|
||||||
|
@ -350,6 +390,63 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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";
|
domain = componentOptions "domain";
|
||||||
|
|
||||||
web = componentOptions "web" // {
|
web = componentOptions "web" // {
|
||||||
|
@ -411,9 +508,160 @@ in
|
||||||
description = "A list of trusted proxies";
|
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 [
|
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
|
# Enable all components if the main server is enabled
|
||||||
(mkIf cfg.enable {
|
(mkIf cfg.enable {
|
||||||
services.firezone.server.domain.enable = true;
|
services.firezone.server.domain.enable = true;
|
||||||
|
@ -447,14 +695,14 @@ in
|
||||||
virtualHosts.${cfg.nginx.webDomain} = {
|
virtualHosts.${cfg.nginx.webDomain} = {
|
||||||
forceSSL = mkDefault true;
|
forceSSL = mkDefault true;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${cfg.dashboardAddress}";
|
proxyPass = "http://${cfg.web.address}:${toString cfg.web.port}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
virtualHosts.${cfg.nginx.apiDomain} = {
|
virtualHosts.${cfg.nginx.apiDomain} = {
|
||||||
forceSSL = mkDefault true;
|
forceSSL = mkDefault true;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${cfg.dashboardAddress}";
|
proxyPass = "http://${cfg.api.address}:${toString cfg.api.port}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -464,7 +712,7 @@ in
|
||||||
{
|
{
|
||||||
services.firezone.server = {
|
services.firezone.server = {
|
||||||
settings = {
|
settings = {
|
||||||
LOG_LEVEL = mkDefault "debug";
|
LOG_LEVEL = mkDefault "info";
|
||||||
RELEASE_HOSTNAME = mkDefault "localhost.localdomain";
|
RELEASE_HOSTNAME = mkDefault "localhost.localdomain";
|
||||||
|
|
||||||
ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd";
|
ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd";
|
||||||
|
@ -483,7 +731,7 @@ in
|
||||||
# sufficient for small to medium deployments
|
# sufficient for small to medium deployments
|
||||||
DATABASE_POOL_SIZE = "16";
|
DATABASE_POOL_SIZE = "16";
|
||||||
|
|
||||||
AUTH_PROVIDER_ADAPTERS = mkDefault "email,openid_connect,userpass,token";
|
AUTH_PROVIDER_ADAPTERS = mkDefault (concatStringsSep "," availableAuthAdapters);
|
||||||
|
|
||||||
FEATURE_FLOW_ACTIVITIES_ENABLED = mkDefault true;
|
FEATURE_FLOW_ACTIVITIES_ENABLED = mkDefault true;
|
||||||
FEATURE_POLICY_CONDITIONS_ENABLED = mkDefault true;
|
FEATURE_POLICY_CONDITIONS_ENABLED = mkDefault true;
|
||||||
|
@ -499,11 +747,13 @@ in
|
||||||
domain.settings = {
|
domain.settings = {
|
||||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9000;
|
ERLANG_DISTRIBUTION_PORT = mkDefault 9000;
|
||||||
HEALTHZ_PORT = mkDefault 4000;
|
HEALTHZ_PORT = mkDefault 4000;
|
||||||
|
BACKGROUND_JOBS_ENABLED = mkDefault true;
|
||||||
};
|
};
|
||||||
|
|
||||||
web.settings = {
|
web.settings = {
|
||||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9001;
|
ERLANG_DISTRIBUTION_PORT = mkDefault 9001;
|
||||||
HEALTHZ_PORT = mkDefault 4001;
|
HEALTHZ_PORT = mkDefault 4001;
|
||||||
|
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||||
|
|
||||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address;
|
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address;
|
||||||
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies);
|
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies);
|
||||||
|
@ -517,6 +767,7 @@ in
|
||||||
api.settings = {
|
api.settings = {
|
||||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9002;
|
ERLANG_DISTRIBUTION_PORT = mkDefault 9002;
|
||||||
HEALTHZ_PORT = mkDefault 4002;
|
HEALTHZ_PORT = mkDefault 4002;
|
||||||
|
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||||
|
|
||||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address;
|
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address;
|
||||||
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.api.trustedProxies);
|
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.api.trustedProxies);
|
||||||
|
@ -528,9 +779,49 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
(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) {
|
(mkIf (cfg.domain.enable || cfg.web.enable || cfg.api.enable) {
|
||||||
# FIXME: mkIf openClusterFirewall {};
|
|
||||||
|
|
||||||
systemd.slices.system-firezone = {
|
systemd.slices.system-firezone = {
|
||||||
description = "Firezone Slice";
|
description = "Firezone Slice";
|
||||||
};
|
};
|
||||||
|
@ -556,13 +847,8 @@ in
|
||||||
${loadSecretEnvironment "domain"}
|
${loadSecretEnvironment "domain"}
|
||||||
|
|
||||||
echo "Running migrations"
|
echo "Running migrations"
|
||||||
${getExe pkgs.firezone-server-domain} eval Domain.Release.migrate
|
${getExe cfg.domain.package} eval Domain.Release.migrate
|
||||||
|
'';
|
||||||
echo "Provisioning"
|
|
||||||
''; # FIXME: ^----- aaaaaaaaaaaaaaaaaa
|
|
||||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
|
||||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
|
||||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
|
||||||
|
|
||||||
# We use the domain environment to be able to run migrations
|
# We use the domain environment to be able to run migrations
|
||||||
environment = collectEnvironment "domain";
|
environment = collectEnvironment "domain";
|
||||||
|
@ -581,7 +867,26 @@ in
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
${loadSecretEnvironment "domain"}
|
${loadSecretEnvironment "domain"}
|
||||||
exec ${getExe pkgs.firezone-server-domain} start;
|
exec ${getExe cfg.domain.package} start;
|
||||||
|
'';
|
||||||
|
|
||||||
|
postStart = mkIf cfg.provision.enable ''
|
||||||
|
${loadSecretEnvironment "domain"}
|
||||||
|
|
||||||
|
# Wait for the firezone server to come online
|
||||||
|
count=0
|
||||||
|
while ! ${getExe cfg.domain.package} pid >/dev/null
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
if [[ "$count" -eq 30 ]]; then
|
||||||
|
echo "Tried for at least 30 seconds, giving up..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
count=$((count++))
|
||||||
|
done
|
||||||
|
|
||||||
|
ln -sTf ${provisionStateJson} provision-state.json
|
||||||
|
${getExe cfg.domain.package} rpc 'Code.eval_file("${./provision.exs}")'
|
||||||
'';
|
'';
|
||||||
|
|
||||||
environment = collectEnvironment "domain";
|
environment = collectEnvironment "domain";
|
||||||
|
@ -597,7 +902,7 @@ in
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
${loadSecretEnvironment "web"}
|
${loadSecretEnvironment "web"}
|
||||||
exec ${getExe pkgs.firezone-server-web} start;
|
exec ${getExe cfg.web.package} start;
|
||||||
'';
|
'';
|
||||||
|
|
||||||
environment = collectEnvironment "web";
|
environment = collectEnvironment "web";
|
||||||
|
@ -613,7 +918,7 @@ in
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
${loadSecretEnvironment "api"}
|
${loadSecretEnvironment "api"}
|
||||||
exec ${getExe pkgs.firezone-server-api} start;
|
exec ${getExe cfg.api.package} start;
|
||||||
'';
|
'';
|
||||||
|
|
||||||
environment = collectEnvironment "api";
|
environment = collectEnvironment "api";
|
||||||
|
|
155
modules/provision.exs
Normal file
155
modules/provision.exs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
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()
|
|
@ -60,7 +60,7 @@ index c4e06bc58..89533fb81 100644
|
||||||
|
|
||||||
- {:error, :send_email, _reason, _effects_so_far} ->
|
- {:error, :send_email, _reason, _effects_so_far} ->
|
||||||
+ {:error, :send_email, reason, _effects_so_far} ->
|
+ {:error, :send_email, reason, _effects_so_far} ->
|
||||||
+ Logger.info("aaaaaaaaaaaaaaaaaa", reason: inspect(reason))
|
+ Logger.info("failed to send email", reason: inspect(reason))
|
||||||
changeset =
|
changeset =
|
||||||
Ecto.Changeset.add_error(
|
Ecto.Changeset.add_error(
|
||||||
changeset,
|
changeset,
|
||||||
|
@ -103,12 +103,12 @@ index 15037e0a3..475c4ddfb 100644
|
||||||
- from_email: compile_config!(:outbound_email_from)
|
- from_email: compile_config!(:outbound_email_from)
|
||||||
+ adapter: compile_config!(:outbound_email_adapter),
|
+ adapter: compile_config!(:outbound_email_adapter),
|
||||||
+ from_email: compile_config!(:outbound_email_from),
|
+ from_email: compile_config!(:outbound_email_from),
|
||||||
+ protocol: :ssl,
|
+ protocol: String.to_atom(System.get_env("OUTBOUND_EMAIL_SMTP_PROTOCOL")),
|
||||||
+ relay: System.get_env("OUTBOUND_EMAIL_RELAY"),
|
+ relay: System.get_env("OUTBOUND_EMAIL_SMTP_HOST"),
|
||||||
+ port: 465,
|
+ port: String.to_integer(System.get_env("OUTBOUND_EMAIL_SMTP_PORT")),
|
||||||
+ auth: [
|
+ auth: [
|
||||||
+ username: System.get_env("OUTBOUND_EMAIL_USERNAME"),
|
+ username: System.get_env("OUTBOUND_EMAIL_SMTP_USERNAME"),
|
||||||
+ password: System.get_env("OUTBOUND_EMAIL_PASSWORD")
|
+ password: System.get_env("OUTBOUND_EMAIL_SMTP_PASSWORD")
|
||||||
+ ]
|
+ ]
|
||||||
] ++ compile_config!(:outbound_email_adapter_opts)
|
] ++ compile_config!(:outbound_email_adapter_opts)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
|
nixosTests,
|
||||||
fetchFromGitHub,
|
fetchFromGitHub,
|
||||||
beamPackages,
|
beamPackages,
|
||||||
pnpm_9,
|
pnpm_9,
|
||||||
|
@ -41,9 +42,6 @@ beamPackages.mixRelease rec {
|
||||||
cat >> config/runtime.exs <<EOF
|
cat >> config/runtime.exs <<EOF
|
||||||
config :tzdata, :data_dir, System.get_env("TZDATA_DIR")
|
config :tzdata, :data_dir, System.get_env("TZDATA_DIR")
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# TODO replace https://firezone.statuspage.io with custom link,
|
|
||||||
# unfortunately simple replace only works at compile time
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postBuild = ''
|
postBuild = ''
|
||||||
|
@ -115,8 +113,12 @@ beamPackages.mixRelease rec {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
passthru.tests = {
|
||||||
|
inherit (nixosTests) firezone;
|
||||||
|
};
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Backend server and Admin UI for the Firezone zero-trust access platform";
|
description = "Backend server for the Firezone zero-trust access platform";
|
||||||
homepage = "https://github.com/firezone/firezone";
|
homepage = "https://github.com/firezone/firezone";
|
||||||
license = lib.licenses.asl20;
|
license = lib.licenses.asl20;
|
||||||
maintainers = with lib.maintainers; [
|
maintainers = with lib.maintainers; [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue