diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 4453c907f67..feb3a8bd2d4 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -1631,6 +1631,13 @@
true.
+
+
+ A module for declarative configuration of openconnect VPN
+ profiles was added under
+ networking.openconnect.
+
+
The element-desktop package now has an
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index abc16b8d66e..9a7ee88e617 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -569,6 +569,8 @@ In addition to numerous new and upgraded packages, this release has the followin
using `fetchgit` or `fetchhg` if the argument `fetchSubmodules`
is set to `true`.
+- A module for declarative configuration of openconnect VPN profiles was added under `networking.openconnect`.
+
- The `element-desktop` package now has an `useKeytar` option (defaults to `true`),
which allows disabling `keytar` and in turn `libsecret` usage
(which binds to native credential managers / keychain libraries).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1ad2592b1dd..813ac397d7f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -851,6 +851,7 @@
./services/networking/ofono.nix
./services/networking/oidentd.nix
./services/networking/onedrive.nix
+ ./services/networking/openconnect.nix
./services/networking/openvpn.nix
./services/networking/ostinato.nix
./services/networking/owamp.nix
diff --git a/nixos/modules/services/networking/openconnect.nix b/nixos/modules/services/networking/openconnect.nix
new file mode 100644
index 00000000000..7b2ef48e1c4
--- /dev/null
+++ b/nixos/modules/services/networking/openconnect.nix
@@ -0,0 +1,135 @@
+{ config, lib, options, pkgs, ... }:
+with lib;
+let
+ cfg = config.networking.openconnect;
+ openconnect = cfg.package;
+ pkcs11 = types.strMatching "pkcs11:.+" // {
+ name = "pkcs11";
+ description = "PKCS#11 URI";
+ };
+ interfaceOptions = {
+ options = {
+ gateway = mkOption {
+ description = "Gateway server to connect to.";
+ example = "gateway.example.com";
+ type = types.str;
+ };
+
+ protocol = mkOption {
+ description = "Protocol to use.";
+ example = "anyconnect";
+ type =
+ types.enum [ "anyconnect" "array" "nc" "pulse" "gp" "f5" "fortinet" ];
+ };
+
+ user = mkOption {
+ description = "Username to authenticate with.";
+ example = "example-user";
+ type = types.nullOr types.str;
+ };
+
+ # Note: It does not make sense to provide a way to declaratively
+ # set an authentication cookie, because they have to be requested
+ # for every new connection and would only work once.
+ passwordFile = mkOption {
+ description = ''
+ File containing the password to authenticate with. This
+ is passed to openconnect
via the
+ --passwd-on-stdin
option.
+ '';
+ default = null;
+ example = "/var/lib/secrets/openconnect-passwd";
+ type = types.nullOr types.path;
+ };
+
+ certificate = mkOption {
+ description = "Certificate to authenticate with.";
+ default = null;
+ example = "/var/lib/secrets/openconnect_certificate.pem";
+ type = with types; nullOr (either path pkcs11);
+ };
+
+ privateKey = mkOption {
+ description = "Private key to authenticate with.";
+ example = "/var/lib/secrets/openconnect_private_key.pem";
+ default = null;
+ type = with types; nullOr (either path pkcs11);
+ };
+
+ extraOptions = mkOption {
+ description = ''
+ Extra config to be appended to the interface config. It should
+ contain long-format options as would be accepted on the command
+ line by openconnect
+ (see https://www.infradead.org/openconnect/manual.html).
+ Non-key-value options like deflate
can be used by
+ declaring them as booleans, i. e. deflate = true;
.
+ '';
+ default = { };
+ example = {
+ compression = "stateless";
+
+ no-http-keepalive = true;
+ no-dtls = true;
+ };
+ type = with types; attrsOf (either str bool);
+ };
+ };
+ };
+ generateExtraConfig = extra_cfg:
+ strings.concatStringsSep "\n" (attrsets.mapAttrsToList
+ (name: value: if (value == true) then name else "${name}=${value}")
+ (attrsets.filterAttrs (_: value: value != false) extra_cfg));
+ generateConfig = name: icfg:
+ pkgs.writeText "config" ''
+ interface=${name}
+ ${optionalString (icfg.user != null) "user=${icfg.user}"}
+ ${optionalString (icfg.passwordFile != null) "passwd-on-stdin"}
+ ${optionalString (icfg.certificate != null)
+ "certificate=${icfg.certificate}"}
+ ${optionalString (icfg.privateKey != null) "sslkey=${icfg.privateKey}"}
+
+ ${generateExtraConfig icfg.extraOptions}
+ '';
+ generateUnit = name: icfg: {
+ description = "OpenConnect Interface - ${name}";
+ requires = [ "network-online.target" ];
+ after = [ "network.target" "network-online.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ ExecStart = "${openconnect}/bin/openconnect --config=${
+ generateConfig name icfg
+ } ${icfg.gateway}";
+ StandardInput = "file:${icfg.passwordFile}";
+ };
+ };
+in {
+ options.networking.openconnect = {
+ package = mkPackageOption pkgs "openconnect" { };
+
+ interfaces = mkOption {
+ description = "OpenConnect interfaces.";
+ default = { };
+ example = {
+ openconnect0 = {
+ gateway = "gateway.example.com";
+ protocol = "anyconnect";
+ user = "example-user";
+ passwordFile = "/var/lib/secrets/openconnect-passwd";
+ };
+ };
+ type = with types; attrsOf (submodule interfaceOptions);
+ };
+ };
+
+ config = {
+ systemd.services = mapAttrs' (name: value: {
+ name = "openconnect-${name}";
+ value = generateUnit name value;
+ }) cfg.interfaces;
+ };
+
+ meta.maintainers = with maintainers; [ alyaeanyx ];
+}