From e54845e6990a7e138bb46b73ad55a61aa3f1df65 Mon Sep 17 00:00:00 2001 From: Brynley Llewellyn-Roux Date: Sun, 25 Feb 2024 11:30:15 +1100 Subject: [PATCH 1/4] feat: added NixOS module configuration feat: initial module feat: added service wip: changed from programs to services wip: removed ExecStop wip: changing from /root/... to /run/keys/... for secret zero wip: module configuration refactoring feat: improved module configuration feat: added recovery option feat: added -dsf CLI option fix: ignore bootstarp failure fix: renamed option to recoveryCodeOutFile fix: minor fixes Refactored code and clarified agent service intentions fix: removed defaults feat: added file permissions checker --- flake.lock | 6 +- flake.nix | 146 ++++++++++++++++++++++++++++++ src/agent/CommandStart.ts | 10 +- src/bootstrap/CommandBootstrap.ts | 26 ++++-- src/utils/options.ts | 6 ++ src/utils/processors.ts | 4 + 6 files changed, 186 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index e77edfb0..2657e4da 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1e5ea336..77b4e031 100644 --- a/flake.nix +++ b/flake.nix @@ -13,7 +13,153 @@ systems = { "x86_64-linux" = [ "linux" "x64" ]; }; + in + { + nixosModules.default = { config, ... }: with nixpkgs; with lib; + { + options = { + services.polykey = { + enable = mkEnableOption "Enable the Polykey agent. Users with the `polykey` group or root permissions will be able to manage the agent."; + + passwordFilePath = mkOption { + type = with types; uniq str; + default = "/this/path/will/cause/a/failure/if/not/set"; + description = '' + The path to the Polykey password file. This is required to be set for the module to work, otherwise this module will fail. + ''; + }; + + recoveryCodeFilePath = mkOption { + type = with types; uniq str; + default = ""; + description = '' + The path to the Polykey recovery code file. This is not required, but if set will read a recovery code from the provided path to bootstrap a new state with. + ''; + }; + + recoveryCodeOutPath = mkOption { + type = with types; uniq str; + description = '' + The path to the Polykey recovery code file output location. + ''; + }; + + statePath = mkOption { + type = with types; uniq str; + default = "/var/lib/polykey"; + description = "The path to the Polykey node state directory. Will default to `/var/lib/polykey`, but can be overwritten to a custom path."; + }; + }; + programs.polykey = { + enable = mkEnableOption "Enable the per-user Polykey agent."; + + passwordFilePath = mkOption { + type = with types; uniq str; + default = "/this/path/will/cause/a/failure/if/not/set"; + description = '' + The path to the Polykey password file. This is required to be set for the module to work, otherwise this module will fail. + ''; + }; + + recoveryCodeFilePath = mkOption { + type = with types; uniq str; + default = ""; + description = '' + The path to the Polykey recovery code file. This is not required, but if set will read a recovery code from the provided path to bootstrap a new state with. + ''; + }; + + recoveryCodeOutPath = mkOption { + type = with types; uniq str; + description = '' + The path to the Polykey recovery code file output location. + ''; + }; + + statePath = mkOption { + type = with types; uniq str; + default = "%h/.local/share/polykey"; + description = "The path to the Polykey node state directory. Will default to `$HOME/.local/share/polykey`, but can be overwritten to a custom path."; + }; + }; + }; + config = mkMerge [ + (mkIf config.services.polykey.enable { + users.groups.polykey = {}; + + environment.systemPackages = [ + self.outputs.packages.${buildSystem}.default + ]; + + system.activationScripts.makeAgentPaths = '' + mkdir -p ${config.services.polykey.statePath} + chgrp -R polykey ${config.services.polykey.statePath} + chmod 770 ${config.services.polykey.statePath} + ''; + + systemd.services.polykey = { + description = "Polykey Agent"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + User = "root"; + Group = "polykey"; + PermissionsStartOnly = true; + LoadCredential = [ + "password:${config.services.polykey.passwordFilePath}" + ]; + ExecStartPre = '' + -${self.outputs.packages.${buildSystem}.default}/bin/polykey \ + --password-file ''${CREDENTIALS_DIRECTORY}/password \ + --node-path ${config.services.polykey.statePath} \ + bootstrap ${lib.optionalString (config.services.polykey.recoveryCodeFilePath != "") '' -rcf ${config.services.polykey.recoveryCodeFilePath}''}\ + --recovery-code-out-file ${config.services.polykey.recoveryCodeOutPath} + ''; + ExecStart = '' + ${self.outputs.packages.${buildSystem}.default}/bin/polykey \ + --password-file ''${CREDENTIALS_DIRECTORY}/password \ + --node-path ${config.services.polykey.statePath} \ + agent start \ + --recovery-code-out-file ${config.services.polykey.recoveryCodeOutPath} + ''; + }; + }; + }) + (mkIf config.programs.polykey.enable { + environment.systemPackages = [ + self.outputs.packages.${buildSystem}.default + ]; + + system.activationScripts.makeUserAgentPaths = '' + mkdir -p ${config.programs.polykey.statePath} + ''; + + systemd.user.services.polykey = { + description = "Polykey Agent"; + wantedBy = [ "default.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStartPre = '' + -${self.outputs.packages.${buildSystem}.default}/bin/polykey \ + --password-file ${config.programs.polykey.passwordFilePath} \ + --node-path ${config.programs.polykey.statePath} \ + bootstrap ${lib.optionalString (config.programs.polykey.recoveryCodeFilePath != "") '' -rcf ${config.programs.polykey.recoveryCodeFilePath}''}\ + --recovery-code-out-file ${config.programs.polykey.recoveryCodeOutPath} + ''; + ExecStart = '' + ${self.outputs.packages.${buildSystem}.default}/bin/polykey \ + --password-file ${config.programs.polykey.passwordFilePath} \ + --node-path ${config.programs.polykey.statePath} \ + agent start \ + --recovery-code-out-file ${config.programs.polykey.recoveryCodeOutPath} + ''; + }; + }; + }) + ]; + }; + } // flake-utils.lib.eachSystem (builtins.attrNames systems) (targetSystem: let platform = builtins.elemAt systems.${targetSystem} 0; diff --git a/src/agent/CommandStart.ts b/src/agent/CommandStart.ts index 8cb8c299..e66e7c6c 100644 --- a/src/agent/CommandStart.ts +++ b/src/agent/CommandStart.ts @@ -12,6 +12,7 @@ import type { DeepPartial } from 'polykey/dist/types'; import type { RecoveryCode } from 'polykey/dist/keys/types'; import childProcess from 'child_process'; import process from 'process'; +import * as fs from 'fs'; import config from 'polykey/dist/config'; import * as keysErrors from 'polykey/dist/keys/errors'; import * as polykeyEvents from 'polykey/dist/events'; @@ -28,6 +29,7 @@ class CommandStart extends CommandPolykey { this.name('start'); this.description('Start the Polykey Agent'); this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.recoveryCodeOutFile); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); this.addOption(binOptions.agentHost); @@ -265,12 +267,18 @@ class CommandStart extends CommandPolykey { type: options.format === 'json' ? 'json' : 'dict', data: { ...statusLiveData!, - ...(recoveryCodeOut != null + ...(recoveryCodeOut != null && options.recoveryCodeOutFile == null ? { recoveryCode: recoveryCodeOut } : {}), }, }), ); + if (options.recoveryCodeOutFile != null && recoveryCodeOut != null) { + await fs.promises.writeFile( + options.recoveryCodeOutFile, + recoveryCodeOut, + ); + } }); } } diff --git a/src/bootstrap/CommandBootstrap.ts b/src/bootstrap/CommandBootstrap.ts index 1662df4f..7be5806b 100644 --- a/src/bootstrap/CommandBootstrap.ts +++ b/src/bootstrap/CommandBootstrap.ts @@ -1,4 +1,5 @@ import process from 'process'; +import * as fs from 'fs'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; import * as binOptions from '../utils/options'; @@ -10,6 +11,7 @@ class CommandBootstrap extends CommandPolykey { this.name('bootstrap'); this.description('Bootstrap Keynode State'); this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.recoveryCodeOutFile); this.addOption(binOptions.fresh); this.addOption(binOptions.privateKeyFile); this.addOption(binOptions.passwordOpsLimit); @@ -37,14 +39,22 @@ class CommandBootstrap extends CommandPolykey { logger: this.logger, }); this.logger.info(`Bootstrapped ${options.nodePath}`); - process.stdout.write( - binUtils.outputFormatter({ - type: options.format === 'json' ? 'json' : 'dict', - data: { - recoveryCode: recoveryCodeOut, - }, - }), - ); + + if (options.recoveryCodeOutFile == null) { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + recoveryCode: recoveryCodeOut, + }, + }), + ); + } else if (recoveryCodeOut != null) { + await fs.promises.writeFile( + options.recoveryCodeOutFile, + recoveryCodeOut, + ); + } }); } } diff --git a/src/utils/options.ts b/src/utils/options.ts index 699affcc..5827cd6d 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -105,6 +105,11 @@ const recoveryCodeFile = new commander.Option( 'Path to Recovery Code', ); +const recoveryCodeOutFile = new commander.Option( + '-rcof, --recovery-code-out-file ', + 'Path to output recovery code. Only used if a `RecoveryCode` is generated.', +); + const background = new commander.Option( '-b, --background', 'Starts the agent as a background process', @@ -240,6 +245,7 @@ export { agentPort, connConnectTime, recoveryCodeFile, + recoveryCodeOutFile, passwordFile, passwordNewFile, background, diff --git a/src/utils/processors.ts b/src/utils/processors.ts index 62937973..4a19cf18 100644 --- a/src/utils/processors.ts +++ b/src/utils/processors.ts @@ -84,6 +84,10 @@ async function processPassword( let password: string | undefined; if (passwordFile != null) { try { + const stats = await fs.promises.stat(passwordFile); + const perms = '0' + (stats.mode & parseInt('777', 8)).toString(8); + if (perms !== '0600') throw new Error('Password file permissions unsafe, expected 0600 permissions'); + password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); } catch (e) { throw new errors.ErrorPolykeyCLIPasswordFileRead(e.message, { From 1332b5d9b5209deb4ac18600ec8b4f876dd14899 Mon Sep 17 00:00:00 2001 From: Brynley Llewellyn-Roux Date: Wed, 27 Mar 2024 18:58:38 +1100 Subject: [PATCH 2/4] fix: reverted file permissions check --- src/utils/processors.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils/processors.ts b/src/utils/processors.ts index 4a19cf18..62937973 100644 --- a/src/utils/processors.ts +++ b/src/utils/processors.ts @@ -84,10 +84,6 @@ async function processPassword( let password: string | undefined; if (passwordFile != null) { try { - const stats = await fs.promises.stat(passwordFile); - const perms = '0' + (stats.mode & parseInt('777', 8)).toString(8); - if (perms !== '0600') throw new Error('Password file permissions unsafe, expected 0600 permissions'); - password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); } catch (e) { throw new errors.ErrorPolykeyCLIPasswordFileRead(e.message, { From 3889994d4e8a78c22d30e89c26d4cc3bfaf4374f Mon Sep 17 00:00:00 2001 From: Brynley Llewellyn-Roux Date: Thu, 28 Mar 2024 11:38:42 +1100 Subject: [PATCH 3/4] fix: removed experimental code --- flake.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/flake.nix b/flake.nix index 77b4e031..3878f4f0 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,6 @@ passwordFilePath = mkOption { type = with types; uniq str; - default = "/this/path/will/cause/a/failure/if/not/set"; description = '' The path to the Polykey password file. This is required to be set for the module to work, otherwise this module will fail. ''; @@ -56,7 +55,6 @@ passwordFilePath = mkOption { type = with types; uniq str; - default = "/this/path/will/cause/a/failure/if/not/set"; description = '' The path to the Polykey password file. This is required to be set for the module to work, otherwise this module will fail. ''; From 02de5c6e1157c40c4ad6dba212b3d64e48d00fa5 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Mon, 8 Apr 2024 13:04:36 +1000 Subject: [PATCH 4/4] fix: review fixes - Fixed importing `* as fs` to just `fs`. - Fixed descriptions of the recovery code file options. Made the distinction between them clear. --- src/agent/CommandStart.ts | 2 +- src/bootstrap/CommandBootstrap.ts | 4 ++-- src/utils/options.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agent/CommandStart.ts b/src/agent/CommandStart.ts index e66e7c6c..31192a4d 100644 --- a/src/agent/CommandStart.ts +++ b/src/agent/CommandStart.ts @@ -12,7 +12,7 @@ import type { DeepPartial } from 'polykey/dist/types'; import type { RecoveryCode } from 'polykey/dist/keys/types'; import childProcess from 'child_process'; import process from 'process'; -import * as fs from 'fs'; +import fs from 'fs'; import config from 'polykey/dist/config'; import * as keysErrors from 'polykey/dist/keys/errors'; import * as polykeyEvents from 'polykey/dist/events'; diff --git a/src/bootstrap/CommandBootstrap.ts b/src/bootstrap/CommandBootstrap.ts index 7be5806b..c4125326 100644 --- a/src/bootstrap/CommandBootstrap.ts +++ b/src/bootstrap/CommandBootstrap.ts @@ -1,5 +1,5 @@ import process from 'process'; -import * as fs from 'fs'; +import fs from 'fs'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; import * as binOptions from '../utils/options'; @@ -53,7 +53,7 @@ class CommandBootstrap extends CommandPolykey { await fs.promises.writeFile( options.recoveryCodeOutFile, recoveryCodeOut, - ); + ); } }); } diff --git a/src/utils/options.ts b/src/utils/options.ts index 5827cd6d..277640da 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -102,12 +102,12 @@ const passwordNewFile = new commander.Option( const recoveryCodeFile = new commander.Option( '-rcf, --recovery-code-file ', - 'Path to Recovery Code', + 'Path to a file used to load the Recovery Code from', ); const recoveryCodeOutFile = new commander.Option( '-rcof, --recovery-code-out-file ', - 'Path to output recovery code. Only used if a `RecoveryCode` is generated.', + 'Path used to write the Recovery Code if one was generated, if none was generated then this is ignored', ); const background = new commander.Option(