diff --git a/modules/misc/news/2025/11/2025-11-04_13-00-00.nix b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix new file mode 100644 index 000000000..8ad7e13dd --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix @@ -0,0 +1,11 @@ +{ + time = "2025-11-04T13:00:00+00:00"; + condition = true; + message = '' + A new module is available: 'programs.opkssh'. + + opkssh is a tool which enables ssh to be used with OpenID Connect allowing SSH access to be managed via identities instead of long-lived SSH keys. It does not replace SSH, but instead generates SSH public keys containing PK Tokens and configures sshd to verify them. These PK Tokens contain standard OpenID Connect ID Tokens. + + This protocol builds on the OpenPubkey which adds user public keys to OpenID Connect without breaking compatibility with existing OpenID Provider. + ''; +} diff --git a/modules/programs/opkssh.nix b/modules/programs/opkssh.nix new file mode 100644 index 000000000..a9d43ea73 --- /dev/null +++ b/modules/programs/opkssh.nix @@ -0,0 +1,59 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.opkssh; + + yamlFormat = pkgs.formats.yaml { }; + +in +{ + meta.maintainers = [ lib.maintainers.swarsel ]; + + options.programs.opkssh = { + enable = lib.mkEnableOption "enable the OpenPubkey SSH client"; + + package = lib.mkPackageOption pkgs "opkssh" { nullable = true; }; + + settings = lib.mkOption { + inherit (yamlFormat) type; + default = { }; + example = lib.literalExpression '' + { + default_provider = "kanidm"; + + providers = [ + { + alias = "kanidm"; + issuer = "https://idm.example.com/oauth2/openid/opkssh"; + client_id = "opkssh"; + scopes = "openid email profile"; + redirect_uris = [ + "http://localhost:3000/login-callback" + "http://localhost:10001/login-callback" + "http://localhost:11110/login-callback" + ]; + }; + ]; + } + ''; + description = '' + Configuration written to {file}`$HOME/.opk/config.yml`. + See . + ''; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; + + home.file."${config.home.homeDirectory}/.opk/config.yml" = lib.mkIf (cfg.settings != { }) { + source = yamlFormat.generate "opkssh-config-${config.home.username}.yml" cfg.settings; + }; + + }; +} diff --git a/tests/modules/programs/opkssh/default.nix b/tests/modules/programs/opkssh/default.nix new file mode 100644 index 000000000..9c6be4912 --- /dev/null +++ b/tests/modules/programs/opkssh/default.nix @@ -0,0 +1,3 @@ +{ + opkssh-basic-config = ./opkssh-basic-config.nix; +} diff --git a/tests/modules/programs/opkssh/opkssh-basic-config.nix b/tests/modules/programs/opkssh/opkssh-basic-config.nix new file mode 100644 index 000000000..a15263b47 --- /dev/null +++ b/tests/modules/programs/opkssh/opkssh-basic-config.nix @@ -0,0 +1,42 @@ +_: { + programs.opkssh = { + enable = true; + settings = { + default_provider = "test-provider"; + providers = [ + { + alias = "test-provider"; + issuer = "https://test.domain/oauth2/openid/opkssh"; + client_id = "opkssh"; + scopes = "openid email profile"; + redirect_uris = [ + "http://localhost:3000/login-callback" + "http://localhost:10001/login-callback" + "http://localhost:11110/login-callback" + ]; + } + ]; + }; + }; + + nmt.script = '' + configFile=home-files/.opk/config.yml + + assertFileExists "$configFile" + + configFileNormalized="$(normalizeStorePaths "$configFile")" + + assertFileContent "$configFileNormalized" ${builtins.toFile "expected.service" '' + default_provider: test-provider + providers: + - alias: test-provider + client_id: opkssh + issuer: https://test.domain/oauth2/openid/opkssh + redirect_uris: + - http://localhost:3000/login-callback + - http://localhost:10001/login-callback + - http://localhost:11110/login-callback + scopes: openid email profile + ''} + ''; +}