Best practice for secure OpenVPN auth-user-pass in NixOS

Hi Guys,

I’m currently setting up OpenVPN as a client in NixOS and want to avoid hardcoding credentials in my configuration or storing them in plaintext files.
My goal is to dynamically provide auth-user-pass to OpenVPN in a secure, declarative way, ideally using pass (Password Store) or another encrypted secret manager.

I’m using a similiar approach with himalaya for email:
in my himalaya/config.toml:
backend.auth.type = “password”
backend.auth.command = “pass show Email/posteo-himalaya”

so if I login, I get asked for my gpg-key password and then the passwort gets returned.

Is there a similiar possibility for the configuration of openvpn. I know the passwort + username is needed at build time.

Current Setup

  • I have an .ovpn file (e.g., xeovo-de-udp.ovpn) with:
auth-user-pass /path/to/vpn-auth.txt
  • The credentials are currently stored in a plaintext file (vpn-auth.txt), which is not ideal for security.

Desired Solution

I want to:

  1. Avoid hardcoding paths like /home/user/nixos/vpn/vpn-auth.txt in my configuration.nix.
  2. Avoid plaintext credentials in files or the Nix store (unless encrypted).
  3. Fetch credentials at build time (e.g., from pass, sops, or age).
  4. Keep the solution declarative (no systemd hacks).

The standard solution to declarative secrets in nixos is to have the secrets encrypted in the config, and include them, still encrypted, in the system generation, then decrypt them during activation with a single key on the target machine that is not declaratively managed. sops-nix and agenix both work in this manner.

I’m not aware of another approach that is properly declarative and also avoids the secrets ending up world-readable in the nix store.

2 Likes

I think defining secrets “fully” declaratively is the wrong approach anyway. Secrets are data, not configuration. You want to be able to rotate them without having to rebuild, that’s an important aspect of something being a secret.

It should be noted that your current approach for himalaya isn’t declarative either - you’re reading the secret at runtime. This is the exact same kind of “hack” you’d perform with systemd credentials, or sops-/agenix.

Given that secrets are inherently data, I don’t think there’s anything wrong with this approach; I honestly think that sops-/agenix are relatively insecure, especially for user-level passwords like this, because they encourage people to share their secrets (albeit in encrypted form). This is a doc issue, to be fair, the upstream sops project explicitly tells you not to do this.

Either way, the only thing they have going over e.g. pass is better integration with NixOS modules, so they’re easy to use and that’s what makes them popular.

I’d like to see a world in which we move to systemd creds, or libsecret for user secrets (though libsecret has its own issues, since it doesn’t check who asks for secrets, I honestly think systemd is the only properly secure secret dissemination utility in the Linux world at the moment).


All that said, if you want to supply secrets from a user to openvpn, IMO the idiomatic approach is to use a networkmanager profile. NetworkManager can be configured to retrieve secrets from a libsecret-style secret agent, and there are ways to make pass such an agent.

This isn’t trivial, though, you’ll have to dig deep into the networkmanager docs.

Alternatively, you can provide your key via the environmentFiles option on a system level, but this cannot integrate with pass - or at least, should not.