Compare commits
2 commits
1730970935
...
2b06848632
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b06848632 | |||
| 3fd32ffa45 |
7 changed files with 324 additions and 0 deletions
30
bin/secret-translator.jl
Normal file
30
bin/secret-translator.jl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env julia
|
||||
using TOML: TOML
|
||||
|
||||
function remove_podman_secrets()
|
||||
run(`podman secret rm --all`)
|
||||
return nothing
|
||||
end #function
|
||||
|
||||
function create_podman_secret(name, secret)
|
||||
run(
|
||||
Cmd(
|
||||
`podman secret create --env=true --replace $name SECRET`;
|
||||
env = Dict("SECRET" => secret),
|
||||
),
|
||||
)
|
||||
return nothing
|
||||
end #function
|
||||
|
||||
function parse_toml_secrets(file)
|
||||
foreach(f -> create_podman_secret(f[1], f[2]), TOML.parsefile(file))
|
||||
return nothing
|
||||
end #function
|
||||
|
||||
function main()
|
||||
remove_podman_secrets()
|
||||
foreach(parse_toml_secrets, ARGS)
|
||||
return 0
|
||||
end #function
|
||||
|
||||
main()
|
||||
|
|
@ -138,9 +138,13 @@
|
|||
|
||||
"mcentire" = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
home-manager-quadlet-nix = quadlet-nix.homeManagerModules.quadlet;
|
||||
};
|
||||
modules = [
|
||||
./systems/linux/mcentire.nix
|
||||
agenix.nixosModules.default
|
||||
home-manager.nixosModules.home-manager
|
||||
quadlet-nix.nixosModules.quadlet
|
||||
crowdsec.nixosModules.crowdsec
|
||||
crowdsec.nixosModules.crowdsec-firewall-bouncer
|
||||
|
|
|
|||
154
modules/podman-secrets.nix
Normal file
154
modules/podman-secrets.nix
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Warning to my future self: This module was "vibe coded" by Claude Sonnet 4.5.
|
||||
# I originally had a hand-written module in here that did the secrets
|
||||
# translation as the root user. I knew that I would want to support multiple
|
||||
# secrets files and multiple users, and thought I should be able to create an
|
||||
# arbitrary number of them, similar to how you can have an arbitrary number
|
||||
# of `programs.firefox.profiles`. Unfortunately, even after looking at lots of
|
||||
# example modules, I could not figure out the syntax (Nix has a serious lack
|
||||
# of minimum working examples), so I broke down and asked Claude to rewrite it
|
||||
# for me. Based on everything that I read, this seems to be exactly what I asked
|
||||
# for.
|
||||
#
|
||||
# Here is the prompt I used to get here:
|
||||
# [@Per service isolation on NixOS with Traefik](zed:///agent/thread/94cb8a22-ff0c-4772-a1a1-018b0107f334?name=Per+service+isolation+on+NixOS+with+Traefik)
|
||||
#
|
||||
# [@podman-secrets.nix](file:///home/millironx/.config/home-manager/modules/podman-secrets.nix)
|
||||
#
|
||||
# I originally wrote this module assuming that I would use Podman as root for
|
||||
# all containers. I would like to fix the module to have as many secrets files
|
||||
# processed as needed with services setup for each secret that is run as the
|
||||
# appropriate user. Ideally, the syntax for working with secrets to be
|
||||
# translated would be
|
||||
#
|
||||
# ```nix
|
||||
# { config, ... }: {
|
||||
# age.secrets = {
|
||||
# "caddy.toml" = {
|
||||
# file = ./../secrets/caddy.toml.age;
|
||||
# owner = "caddy";
|
||||
# group = "caddy";
|
||||
# };
|
||||
#
|
||||
# "authentik.toml" = {
|
||||
# file = ./../secrets/authentik.toml.age;
|
||||
# owner = "authentik";
|
||||
# group = "authentik";
|
||||
# };
|
||||
#
|
||||
# "freshrss.toml" = {
|
||||
# file = ./../secrets/freshrss.toml.age;
|
||||
# owner = "freshrss";
|
||||
# group = "freshrss";
|
||||
# };
|
||||
# };
|
||||
#
|
||||
# millironx.podman-secrets = with config; {
|
||||
# caddy = {
|
||||
# user = "caddy";
|
||||
# secrets-files = [
|
||||
# ./../not-really-secret.toml
|
||||
# age.secrets."caddy.toml".path
|
||||
# ];
|
||||
# };
|
||||
# authentik = {
|
||||
# user = "authentik";
|
||||
# secrets-files = [
|
||||
# age.secrets."authentik.toml".path
|
||||
# ];
|
||||
# };
|
||||
# freshrss = {
|
||||
# user = "freshrss";
|
||||
# secrets-files = [
|
||||
# age.secrets."freshrss.toml".path
|
||||
# ];
|
||||
# };
|
||||
# };
|
||||
# }
|
||||
# ```
|
||||
#
|
||||
# Can you help me rewrite the module to accomplish this?
|
||||
#
|
||||
{ config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.millironx.podman-secrets;
|
||||
|
||||
secret-translator = pkgs.writeScriptBin "secret-translator"
|
||||
(builtins.readFile ./../bin/secret-translator.jl);
|
||||
|
||||
# Submodule type for each service's secrets configuration
|
||||
serviceSecretsType = types.submodule {
|
||||
options = {
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
description = "User account to run the secrets translation service as";
|
||||
example = "caddy";
|
||||
};
|
||||
|
||||
secrets-files = mkOption {
|
||||
type = types.listOf (types.either types.path types.string);
|
||||
description =
|
||||
"List of TOML files containing secrets to translate to Podman secrets";
|
||||
example = literalExpression ''
|
||||
[
|
||||
"/run/agenix/caddy.toml"
|
||||
./../not-really-secret.toml
|
||||
]
|
||||
'';
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Generate a systemd service for each configured service
|
||||
mkSecretsService = name: serviceCfg:
|
||||
nameValuePair "podman-secrets-${name}" {
|
||||
description = "Podman secrets converter service for ${name}";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = serviceCfg.user;
|
||||
ExecStart = "${secret-translator}/bin/secret-translator ${
|
||||
lib.concatStringsSep " " serviceCfg.secrets-files
|
||||
}";
|
||||
Path = [ "${pkgs.podman}/bin" "${pkgs.julia-lts-bin}/bin" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Filter out services with no secrets-files
|
||||
enabledServices =
|
||||
lib.filterAttrs (name: serviceCfg: serviceCfg.secrets-files != [ ]) cfg;
|
||||
|
||||
in {
|
||||
options.millironx.podman-secrets = mkOption {
|
||||
type = types.attrsOf serviceSecretsType;
|
||||
description = ''
|
||||
Per-service Podman secrets configuration.
|
||||
Each attribute creates a separate systemd service that translates TOML secrets
|
||||
files into Podman secrets for the specified user.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
caddy = {
|
||||
user = "caddy";
|
||||
secrets-files = [
|
||||
./../not-really-secret.toml
|
||||
config.age.secrets."caddy.toml".path
|
||||
];
|
||||
};
|
||||
authentik = {
|
||||
user = "authentik";
|
||||
secrets-files = [
|
||||
config.age.secrets."authentik.toml".path
|
||||
];
|
||||
};
|
||||
}
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
|
||||
config = mkIf (enabledServices != { }) {
|
||||
systemd.services = lib.mapAttrs' mkSecretsService enabledServices;
|
||||
};
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ let
|
|||
|
||||
in {
|
||||
"secrets/ansible-vault-password.age".publicKeys = system-administrators;
|
||||
"secrets/authentik.toml.age".publicKeys = system-administrators
|
||||
++ [ mcentire-host ];
|
||||
"secrets/borgmatic-passphrase.age".publicKeys = system-administrators
|
||||
++ [ mcentire-host ];
|
||||
"secrets/borgmatic-ssh-config.age".publicKeys = system-administrators
|
||||
|
|
|
|||
BIN
secrets/authentik.toml.age
Normal file
BIN
secrets/authentik.toml.age
Normal file
Binary file not shown.
128
services/authentik.nix
Normal file
128
services/authentik.nix
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
{ config, home-manager-quadlet-nix, ... }:
|
||||
let
|
||||
user = "authentik";
|
||||
state-directory = "/var/lib/authentik";
|
||||
port = "9000";
|
||||
|
||||
in {
|
||||
# Secrets are translated in the system scope, but performed by the user,
|
||||
# so are available in the user's Podman secrets
|
||||
age.secrets = {
|
||||
"authentik.toml" = {
|
||||
file = ./../secrets/authentik.toml.age;
|
||||
owner = "${user}";
|
||||
};
|
||||
};
|
||||
|
||||
millironx.podman-secrets.authentik = {
|
||||
user = "${user}";
|
||||
secrets-files = [ config.age.secrets."authentik.toml".path ];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [ "d ${state-directory} 1775 ${user} ${user} -" ];
|
||||
|
||||
services.caddy.virtualHosts."auth.millironx.com".extraConfig = ''
|
||||
reverse_proxy http://127.0.0.1:${port}
|
||||
'';
|
||||
|
||||
# Create a dedicated user for this service
|
||||
users.users."${user}" = {
|
||||
group = "${user}";
|
||||
isSystemUser = true;
|
||||
linger = true;
|
||||
autoSubUidGidRange = true;
|
||||
};
|
||||
users.groups."${user}" = {};
|
||||
|
||||
home-manager.users."${user}" = { config, ... }: {
|
||||
imports = [ home-manager-quadlet-nix ];
|
||||
|
||||
home.stateVersion = "25.05";
|
||||
|
||||
virtualisation.quadlet.containers = {
|
||||
authentik-db = {
|
||||
autoStart = true;
|
||||
containerConfig = {
|
||||
image = "docker.io/library/postgres:16";
|
||||
environments = {
|
||||
POSTGRES_DB = "${user}";
|
||||
POSTGRES_USER = "${user}";
|
||||
};
|
||||
secrets = [
|
||||
"AUTHENTIK_POSTGRESQL__PASSWORD,type=env,target=POSTGRES_PASSWORD"
|
||||
];
|
||||
healthCmd = "pg_isready -d \${POSTGRES_DB} -U \${POSTGRES_USER}";
|
||||
healthInterval = "30s";
|
||||
healthRetries = 5;
|
||||
healthStartPeriod = "20s";
|
||||
volumes = [ "${state-directory}/database:/var/lib/postgresql/data" ];
|
||||
};
|
||||
};
|
||||
|
||||
authentik-worker = {
|
||||
autoStart = true;
|
||||
containerConfig = {
|
||||
image = "ghcr.io/goauthentik/server:2025.10.2";
|
||||
environments = {
|
||||
AUTHENTIK_POSTGRESQL__HOST = "authentik-db";
|
||||
AUTHENTIK_POSTGRESQL__NAME = "${user}";
|
||||
AUTHENTIK_POSTGRESQL__USER = "${user}";
|
||||
};
|
||||
exec = "worker";
|
||||
secrets = [
|
||||
"AUTHENTIK_POSTGRESQL__PASSWORD,type=env"
|
||||
"AUTHENTIK_SECRET_KEY,type=env"
|
||||
];
|
||||
volumes = [
|
||||
"${state-directory}/media:/media"
|
||||
"${state-directory}/custom-templates:/templates"
|
||||
"${state-directory}/certs:/certs"
|
||||
];
|
||||
};
|
||||
unitConfig.Requires = [
|
||||
config.virtualisation.quadlet.containers.authentik-db.ref
|
||||
"network-online.target"
|
||||
];
|
||||
unitConfig.After = [
|
||||
config.virtualisation.quadlet.containers.authentik-db.ref
|
||||
"network-online.target"
|
||||
];
|
||||
};
|
||||
|
||||
authentik = {
|
||||
autoStart = true;
|
||||
containerConfig = {
|
||||
image = "ghcr.io/goauthentik/server:2025.10.2";
|
||||
environments = {
|
||||
AUTHENTIK_POSTGRESQL__HOST = "authentik-db";
|
||||
AUTHENTIK_POSTGRESQL__NAME = "${user}";
|
||||
AUTHENTIK_POSTGRESQL__USER = "${user}";
|
||||
};
|
||||
secrets = [
|
||||
"AUTHENTIK_POSTGRESQL__PASSWORD,type=env"
|
||||
"AUTHENTIK_SECRET_KEY,type=env"
|
||||
"AUTHENTIK_EMAIL__HOST,type=env"
|
||||
"AUTHENTIK_EMAIL__PORT,type=env"
|
||||
"AUTHENTIK_EMAIL__USERNAME,type=env"
|
||||
"AUTHENTIK_EMAIL__PASSWORD,type=env"
|
||||
"AUTHENTIK_EMAIL__USE_SSL,type=env"
|
||||
"AUTHENTIK_EMAIL__FROM,type=env"
|
||||
];
|
||||
publishPorts = [ "127.0.0.1:${port}:${port}" ];
|
||||
volumes = [
|
||||
"${state-directory}/media:/media"
|
||||
"${state-directory}/custom-templates:/templates"
|
||||
];
|
||||
};
|
||||
unitConfig.Requires = [
|
||||
config.virtualisation.quadlet.containers.authentik-db.ref
|
||||
"network-online.target"
|
||||
];
|
||||
unitConfig.After = [
|
||||
config.virtualisation.quadlet.containers.authentik-db.ref
|
||||
"network-online.target"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@
|
|||
{
|
||||
imports = [ # Include the results of the hardware scan.
|
||||
./hardware-configuration/mcentire.nix
|
||||
./../../modules/podman-secrets.nix
|
||||
./../../services/nixos-update.nix
|
||||
./../../services/borgmatic.nix
|
||||
./../../services/crowdsec.nix
|
||||
./../../services/authentik.nix
|
||||
];
|
||||
|
||||
# Use the GRUB 2 boot loader.
|
||||
|
|
@ -16,6 +18,7 @@
|
|||
useDHCP = false;
|
||||
interfaces.eth0.useDHCP = true;
|
||||
hostName = "mcentire"; # Define your hostname.
|
||||
firewall.allowedTCPPorts = [ 80 443 ];
|
||||
};
|
||||
|
||||
# Set your time zone.
|
||||
|
|
@ -55,8 +58,11 @@
|
|||
services = {
|
||||
openssh.enable = true;
|
||||
tailscale.enable = true;
|
||||
caddy.enable = true;
|
||||
};
|
||||
|
||||
virtualisation.quadlet.enable = true;
|
||||
|
||||
system.stateVersion = "25.05"; # Did you read the comment?
|
||||
nix = { extraOptions = "experimental-features = nix-command flakes"; };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue