{
config,
lib,
nodes,
...
}:
let
cfg = config.mjm.authelia;
in
{
options.mjm.authelia = {
enable = lib.mkEnableOption "authelia";
};
imports = [ ./users.nix ];
config = lib.mkIf cfg.enable {
<<config>>
};
_class = "nixos";
}When you run a bunch of different self-hosted services, it's really convenient to not have to set up users on each one individually. A single sign-on service is really useful, and I like Authelia for that purpose. It supports both OpenID Connect (preferred) as well as a proxy mode where requests go through Authelia to check authentication before getting through to an actual service. The latter is largely implemented in the ingress service module.
mjm.services.authelia = {
<<service-settings>>
};Authelia uses several parts of my common service infrastructure:
http = {
port = 9191;
health.path = "/api/health";
metrics.enable = true;
metrics.port = 9959;
ingress = {
subdomain = "auth";
authMode = "none";
};
};Authelia is exposed externally at https://auth.midna.dev, though it's mostly accessed this way via redirects from other sites. It has a dedicated health check endpoint that Consul users. It can expose Prometheus metrics, but only on a dedicated port. And of course, since it's the one providing authentication for everything else, it doesn't have the auth proxy or OIDC client setup.
postgresql.enable = true;
Authelia stores its data in a PostgreSQL database.
secrets.enable = true;
Authelia requires several different secrets that are all stored in Vault.
services.authelia.instances."" = {
enable = true;
<<authelia-instance>>
};On NixOS, Authelia is configured via multiple instances. This is because, although I don't think it's the case anymore, you used to need a separate Authelia instance for each domain you wanted it to protect. Regardless, I only have a single instance, which is used to protect *.midna.dev. Many of the rest of the options shown here are set within the context of this single instance.
settings.theme = "dark"; settings.server.address = "tcp://127.0.0.1:9191";
I'm using the dark theme, since it's what I use on most of my devices, and I don't really want my phone to flashbang me in the dark. The default in NixOS is for Authelia to listen on all IPs, but since it will be served over a tunnel, I want it to only listen locally.
settings.telemetry.metrics.enabled = true;
The same is true for metrics, which are served on a distinct port that needs to be enabled. Here, the default from NixOS is fine.
settings.storage.postgres = {
address = "unix:///run/postgresql";
database = "authelia";
username = "authelia";
password = "authelia";
};Authelia needs to be configured to be able to access PostgreSQL. The username needs to match the Unix username used, which is authelia-main. And on NixOS, it's easiest to set up if the database name matches the username. The password is unimportant, but I think Authelia complains if one isn't set for some reason.
settings.session.cookies = [
{
domain = "midna.dev";
authelia_url = "https://auth.midna.dev";
default_redirection_url = "https://launch.midna.dev";
}
];
settings.session.redis.host = "${config.services.redis.servers.authelia.unixSocket}";services.redis.servers.authelia.enable = true; systemd.services.authelia.serviceConfig.SupplementaryGroups = [ config.services.redis.servers.authelia.user ];
Authelia also needs a place to store session information. If you want that information to persist across restarts of Authelia, that needs to be Redis. I've run Authelia before with in-memory sessions to try to avoid the complexity, but having it lose sessions that easily gets old very quickly.
The session settings are also where you configure which domains Authelia is responsible for, and where Authelia is accessible for each. This is important due to the way cookies work between domains, as using Authelia as a proxy requires cookies set by the Authelia domain be visible from other sites on the domain it's protecting.
settings.notifier.smtp = {
address = "submission://smtp.fastmail.com:587";
username = "matt@mattmoriarity.com";
sender = "Authelia <admin@mattmoriarity.com>";
};
environmentVariables.AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = "%d/authelia.smtp_password";systemd.services.authelia.credentials.authelia = [ "smtp_password" ];
Authelia can be configured to be able to send emails. I think this is used for handling forgotten passwords, but I'm not sure I've ever actually used it. But I still have it configured. For some reason, the NixOS module doesn't have a dedicated option for managing this secret, so I have to do it by hand, which isn't that bad.
settings.identity_providers.oidc = {
cors.endpoints = [
"token"
"userinfo"
];
clients =
(
nodes
|> lib.attrValues
|> map (n: n.config.mjm.services)
|> lib.mergeAttrsList
|> lib.attrValues
|> lib.filter (s: s.http.ingress.subdomain != null && s.http.ingress.authMode == "oidc")
|> map (s: s.http.ingress.oidc.clientConfig)
)
++ [
<<opencloud-android-client>>
];
};The OpenID Connect clients for Authelia are configured automatically based on the configuration each service includes in mjm.services, and this is aggregated across all nodes. This means adding a new service updates Authelia without the need to edit Authelia's config.
{
client_id = "OpenCloudAndroid";
client_name = "OpenCloud (Android)";
client_secret = "";
public = true;
redirect_uris = [
"oc://android.opencloud.eu"
];
scopes = [
"openid"
"profile"
"groups"
"email"
"offline_access"
];
grant_types = [
"authorization_code"
"refresh_token"
];
require_pkce = true;
pkce_challenge_method = "S256";
}There is one OIDC client that is included here directly instead of in the corresponding service. The mjm.services option only allows defining a single OIDC client for a service, but OpenCloud has an additional one for its Android app, so that is just added here.
settings.access_control = {
default_policy = "two_factor";
rules =
nodes
|> lib.attrValues
|> map (n: n.config.mjm.services)
|> lib.mergeAttrsList
|> lib.attrValues
|> lib.filter (s: s.http.ingress.subdomain != null && s.http.ingress.authMode == "proxy")
|> lib.concatMap (
s: map (r: { domain = "${s.http.ingress.subdomain}.midna.dev"; } // r) s.http.ingress.proxy.rules
);
};By default, I require two-factor authentication to access services. That just seems like a good practice to me. The access control settings are also where rules for the forward proxy are defined. Like the OIDC clients above, these are aggregated from the configuration for each service. Each one gets the domain set appropriately based on the ingress subdomain.
settings.default_2fa_method = "webauthn";
settings.webauthn = {
enable_passkey_login = true;
experimental_enable_passkey_uv_two_factors = true;
display_name = "MJ's Homelab";
attestation_conveyance_preference = "direct";
};I've configured my Authelia setup to let me use passkeys to login. The experimental option lets a passkey count as both factors.
secrets = lib.genAttrs [ "jwtSecretFile" "oidcHmacSecretFile" "oidcIssuerPrivateKeyFile" "sessionSecretFile" "storageEncryptionKeyFile" ] (_: "/run/authelia-creds.sock");
mjm.spire.creds.authelia.aliases = {
"authelia.service/jwtSecretFile" = "authelia/jwt_secret";
"authelia.service/oidcHmacSecretFile" = "authelia/hmac_secret";
"authelia.service/oidcIssuerPrivateKeyFile" = "authelia/jwt_private_key";
"authelia.service/sessionSecretFile" = "authelia/session_secret";
"authelia.service/storageEncryptionKeyFile" = "authelia/storage_encryption_key";
};The NixOS module is already set up to get secrets from systemd credentials, which is great because that's the method I use to get secrets from Vault. For my case, every credential should come from the credential socket, and then I can configure aliases for each fixed credential name the module uses so that they point at the correct secret in Vault.
systemd.services.authelia.after = [ "redis-authelia.service" "postgresql.service" ];
Finally, Authelia depends on both PostgreSQL and Redis, so it should start after each of them.
text/gemini;lang=en-USThis content has been proxied by September (UNKNO).