ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/

limit and document valid deployment users (#335)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

marshmallow
autofix-ci[bot]
and committed by
GitHub
f1a2429e f6e2e26c

+71 -55
-1
CHANGELOG.md
··· 15 15 - `--path` now supports flakerefs (`github:foo/bar`, `git+file:///...`, 16 16 `https://.../main.tar.gz`, etc). 17 17 - `--flake` is now an alias for `--path`. 18 - - wire will now attempt to use SSH `ControlMaster` by default. 19 18 - A terminal bell will be output if a sudo / ssh prompt is ever printed. 20 19 21 20 ### Fixed
+1
doc/.vitepress/config.ts
··· 90 90 items: [ 91 91 { text: "Install wire", link: "/guides/installation" }, 92 92 { text: "Apply your Config", link: "/guides/apply" }, 93 + { text: "Use a non-root user", link: "/guides/non-root-user" }, 93 94 { text: "Target Nodes", link: "/guides/targeting" }, 94 95 { 95 96 text: "Flakes",
+54
doc/guides/non-root-user.md
··· 1 + --- 2 + comment: true 3 + title: Use a non-root user 4 + description: Deploy as any user with wire. 5 + --- 6 + 7 + # {{ $frontmatter.title }} 8 + 9 + {{ $frontmatter.description }} 10 + 11 + ## Deploying User Requirements 12 + 13 + If your selected deployment user does not fit the following requirements, the 14 + deployment commands will likely fail with an error: 15 + 16 + | | Password-based SSH | Non-interactive SSH Auth | 17 + | :--------------------------------- | -----------------: | -----------------------: | 18 + | In `wheel` (Sudo User) | ❌ Not Supported | ✅ Supported | 19 + | Not In `wheel` (Unprivileged user) | ❌ Not Supported | ❌ Not Supported | 20 + 21 + - "In `wheel`" here meaning a sudoer, whether it be `root` or not. 22 + - "Non-interactive SSH Auth" here most likely meaning an SSH key, anything that 23 + does not require keyboard input in the terminal. 24 + 25 + ## Changing the user 26 + 27 + By default, the target is set to root: 28 + 29 + ```nix 30 + { 31 + deployment.target.user = "root"; 32 + } 33 + ``` 34 + 35 + But it can be any user you want so long as it fits the requirements above. 36 + 37 + ```nix 38 + { 39 + deployment.target.user = "root"; # [!code --] 40 + deployment.target.user = "deploy-user"; # [!code ++] 41 + } 42 + ``` 43 + 44 + After this change, wire will prompt you for sudo authentication, and tell you 45 + the exact command wire wants privileged. 46 + 47 + ```sh{6} 48 + $ wire apply keys --on media 49 + INFO eval_hive: evaluating hive Flake("/path/to/hive") 50 + ... 51 + INFO media | step="Upload key @ NoFilter" progress="3/4" 52 + me@node:22 | Authenticate for "sudo /nix/store/.../bin/key_agent": 53 + [sudo] password for deploy-user: 54 + ```
+1 -1
flake.nix
··· 42 42 ./doc 43 43 ./tests/nix 44 44 ./bench/run.nix 45 + ./runtime 45 46 ]; 46 47 systems = import systems; 47 48 48 49 flake = { 49 - nixosModules.default = import ./runtime/module; 50 50 makeHive = import ./runtime/makeHive.nix; 51 51 hydraJobs = 52 52 let
+3
runtime/default.nix
··· 1 + { 2 + flake.nixosModules.default = import ./module; 3 + }
+2 -1
runtime/module/options.nix
··· 35 35 }; 36 36 user = lib.mkOption { 37 37 type = types.str; 38 - description = "User to use for ssh."; 38 + description = "User to use for SSH. The user must be atleast `wheel` and must use an SSH key or similar 39 + non-interactive login method. More information can be found at https://wire.althaea.zone/guides/non-root-user"; 39 40 default = "root"; 40 41 }; 41 42 port = lib.mkOption {
+10 -52
wire/lib/src/hive/node.rs
··· 6 6 use gethostname::gethostname; 7 7 use serde::{Deserialize, Serialize}; 8 8 use std::assert_matches::debug_assert_matches; 9 - use std::env; 10 9 use std::fmt::Display; 11 - use std::io::ErrorKind; 12 - use std::path::PathBuf; 13 10 use std::sync::Arc; 14 11 use tokio::sync::oneshot; 15 12 use tracing::{Instrument, Level, Span, debug, error, event, instrument, trace}; 16 13 17 14 use crate::commands::common::evaluate_hive_attribute; 18 15 use crate::commands::{CommandArguments, WireCommandChip, run_command}; 19 - use crate::errors::{CommandError, NetworkError}; 16 + use crate::errors::NetworkError; 20 17 use crate::hive::HiveLocation; 21 18 use crate::hive::steps::build::Build; 22 - use crate::hive::steps::cleanup::{CleanUp, clean_up_control_master}; 19 + use crate::hive::steps::cleanup::CleanUp; 23 20 use crate::hive::steps::evaluate::Evaluate; 24 21 use crate::hive::steps::keys::{Key, Keys, PushKeyAgent, UploadKeyAt}; 25 22 use crate::hive::steps::ping::Ping; ··· 77 74 .to_string(), 78 75 ]; 79 76 80 - if modifiers.non_interactive || non_interactive_forced { 81 - options.extend(["PasswordAuthentication=no".to_string()]); 82 - options.extend(["KbdInteractiveAuthentication=no".to_string()]); 83 - } 84 - 85 - let control_path = get_control_path().map_err(HiveLibError::CommandError)?; 86 - options.extend([ 87 - format!("ControlMaster={}", if master { "yes" } else { "no" }), 88 - format!("ControlPath={control_path}"), 89 - "ControlPersist=yes".to_string(), 90 - ]); 77 + options.extend(["PasswordAuthentication=no".to_string()]); 78 + options.extend(["KbdInteractiveAuthentication=no".to_string()]); 91 79 92 80 vector.push("-o".to_string()); 93 81 vector.extend(options.into_iter().intersperse("-o".to_string())); 94 82 95 83 Ok(vector) 96 - } 97 - } 98 - 99 - fn get_control_path() -> Result<String, CommandError> { 100 - match env::var("XDG_RUNTIME_DIR") { 101 - Ok(runtime_dir) => { 102 - let control_path = PathBuf::from(runtime_dir).join("wire"); 103 - 104 - match std::fs::create_dir(&control_path) { 105 - Err(err) if err.kind() != ErrorKind::AlreadyExists => { 106 - return Err(CommandError::RuntimeDirectory(err)); 107 - } 108 - _ => (), 109 - } 110 - 111 - Ok(control_path.join("%C").display().to_string()) 112 - } 113 - Err(err) => Err(CommandError::RuntimeDirectoryMissing(err)), 114 84 } 115 85 } 116 86 ··· 227 197 } 228 198 } 229 199 230 - /// Tests the connection to a node, and sets up an SSH control master process in the background 200 + /// Tests the connection to a node 231 201 pub async fn ping(&self, modifiers: SubCommandModifiers) -> Result<(), HiveLibError> { 232 - let _ = clean_up_control_master(self, modifiers).await; 233 - 234 202 let host = self.target.get_preferred_host()?; 235 203 236 204 let command_string = format!( 237 - "ssh {}@{host} {} -N", 205 + "ssh {}@{host} {} exit", 238 206 self.target.user, 239 207 self.target.create_ssh_opts(modifiers, true)? 240 208 ); ··· 718 686 "-o".to_string(), 719 687 "StrictHostKeyChecking=accept-new".to_string(), 720 688 "-o".to_string(), 721 - "ControlMaster=no".to_string(), 722 - "-o".to_string(), 723 - format!("ControlPath={tmp}/wire/%C"), 689 + "PasswordAuthentication=no".to_string(), 724 690 "-o".to_string(), 725 - "ControlPersist=yes".to_string(), 691 + "KbdInteractiveAuthentication=no".to_string(), 726 692 ]; 727 693 728 694 assert_eq!( ··· 748 714 "-o".to_string(), 749 715 "StrictHostKeyChecking=accept-new".to_string(), 750 716 "-o".to_string(), 751 - "ControlMaster=yes".to_string(), 752 - "-o".to_string(), 753 - format!("ControlPath={tmp}/wire/%C"), 717 + "PasswordAuthentication=no".to_string(), 754 718 "-o".to_string(), 755 - "ControlPersist=yes".to_string(), 719 + "KbdInteractiveAuthentication=no".to_string(), 756 720 ] 757 721 ); 758 722 ··· 771 735 "PasswordAuthentication=no".to_string(), 772 736 "-o".to_string(), 773 737 "KbdInteractiveAuthentication=no".to_string(), 774 - "-o".to_string(), 775 - "ControlMaster=yes".to_string(), 776 - "-o".to_string(), 777 - format!("ControlPath={tmp}/wire/%C"), 778 - "-o".to_string(), 779 - "ControlPersist=yes".to_string(), 780 738 ] 781 739 ); 782 740