From 6ed8b3d3e35628a9cc20ec37820d85e01955239b Mon Sep 17 00:00:00 2001 From: Manuel Bluhm Date: Fri, 18 Oct 2024 04:26:48 +0400 Subject: [PATCH] Enable userborn - enable userborn across all VMs and host - add impermanence path to fix user home permissions - persist /etc for gui-vm - change 'password' to 'initialPassword' option to allow updating the system without resetting the passwords - add a proxyuser for dbus over ssh functionality (temporary?) This version persists the entire /etc of the guivm. A second version persists only shadow, passwd, and group as required by userborn. This is better, but may require more work to use tools like usermod later, if required. Both versions support user updates through configuration. Signed-off-by: Manuel Bluhm --- flake.lock | 7 +- flake.nix | 2 +- modules/common/services/xdgopener.nix | 2 +- modules/common/users/accounts.nix | 66 +++++++++++++++++-- modules/givc/appvm.nix | 4 +- .../virtualization/microvm/adminvm.nix | 15 ++++- .../microvm/virtualization/microvm/appvm.nix | 17 ++--- .../virtualization/microvm/audiovm.nix | 36 +++++----- .../microvm/common/storagevm.nix | 28 +++++--- .../microvm/virtualization/microvm/guivm.nix | 30 +++++---- .../virtualization/microvm/microvm-host.nix | 35 +++++----- .../microvm/virtualization/microvm/netvm.nix | 30 +++++---- packages/bt-launcher/default.nix | 4 +- packages/nm-launcher/default.nix | 4 +- 14 files changed, 182 insertions(+), 98 deletions(-) diff --git a/flake.lock b/flake.lock index d86135f54..ac23e475f 100644 --- a/flake.lock +++ b/flake.lock @@ -315,16 +315,17 @@ }, "impermanence": { "locked": { - "lastModified": 1727649413, - "narHash": "sha256-FA53of86DjFdeQzRDVtvgWF9o52rWK70VHGx0Y8fElQ=", + "lastModified": 1728049659, + "narHash": "sha256-lGtad92Y/TnqpXRlZ1syiEq5czpvblKmcypeqGPiVF4=", "owner": "nix-community", "repo": "impermanence", - "rev": "d0b38e550039a72aff896ee65b0918e975e6d48e", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" }, "original": { "owner": "nix-community", "repo": "impermanence", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 14b150def..9a469d1c6 100644 --- a/flake.nix +++ b/flake.nix @@ -139,7 +139,7 @@ }; impermanence = { - url = "github:nix-community/impermanence"; + url = "github:nix-community/impermanence/32b1094d28d5fbedcc85a403bc08c8877b396255"; }; givc = { diff --git a/modules/common/services/xdgopener.nix b/modules/common/services/xdgopener.nix index a7e7985d4..080096353 100644 --- a/modules/common/services/xdgopener.nix +++ b/modules/common/services/xdgopener.nix @@ -53,7 +53,7 @@ in serviceConfig = { # The user 'ghaf' is used here to access SSH keys for the scp command # This is required to copy files to the zathuravm - User = "ghaf"; + User = "${config.ghaf.users.accounts.user}"; ExecStart = "${ghaf-xdg-open}/bin/ghaf-xdg-open"; StandardInput = "socket"; StandardOutput = "journal"; diff --git a/modules/common/users/accounts.nix b/modules/common/users/accounts.nix index 3c337101f..cc7ce3dc5 100644 --- a/modules/common/users/accounts.nix +++ b/modules/common/users/accounts.nix @@ -20,7 +20,13 @@ in { #TODO Extend this to allow definition of multiple users options.ghaf.users.accounts = { - enable = mkEnableOption "Default account Setup"; + enable = mkOption { + default = true; + type = types.bool; + description = '' + Enable Ghaf user accounts. Defaults to true. + ''; + }; user = mkOption { default = "ghaf"; type = types.str; @@ -28,16 +34,16 @@ in The admin account with sudo rights. ''; }; - password = mkOption { + initialPassword = mkOption { default = "ghaf"; type = types.str; description = '' - Default password for the admin user. + Default password for the admin and login user accounts. ''; }; enableLoginUser = mkEnableOption "Enable login user setup for UI."; loginuser = mkOption { - default = "manuel"; + default = "user"; type = types.str; description = '' Default user account for UI. @@ -50,16 +56,40 @@ in Default UID for the login user. ''; }; + # TODO Remove proxy user with ssh functionality + enableProxyUser = mkEnableOption "Enable proxy for login user."; + proxyuser = mkOption { + default = "proxyuser"; + type = types.str; + description = '' + Default user account for dbus proxy functionality. + ''; + }; + proxyuserGroups = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + Extra groups for the proxy user. + ''; + }; }; config = mkIf cfg.enable { + + assertions = [ + { + assertion = !(cfg.enableLoginUser && cfg.enableProxyUser); + message = "You cannot enable both login and proxy users at the same time"; + } + ]; + users = { mutableUsers = cfg.enableLoginUser; users = { "${cfg.user}" = { isNormalUser = true; - inherit (cfg) password; + inherit (cfg) initialPassword; extraGroups = [ "wheel" @@ -73,11 +103,19 @@ in "${cfg.loginuser}" = { isNormalUser = true; uid = cfg.loginuid; - inherit (cfg) password; + inherit (cfg) initialPassword; extraGroups = [ "video" ]; }; + } + // optionalAttrs cfg.enableProxyUser { + "${cfg.proxyuser}" = { + isNormalUser = true; + createHome = false; + uid = cfg.loginuid; + extraGroups = cfg.proxyuserGroups; + }; }; groups = { @@ -91,11 +129,25 @@ in name = cfg.loginuser; members = [ cfg.loginuser ]; }; + } + // optionalAttrs cfg.enableProxyUser { + "${cfg.proxyuser}" = { + name = cfg.proxyuser; + members = [ cfg.proxyuser ]; + }; }; }; # to build ghaf as ghaf-user with caches nix.settings.trusted-users = mkIf config.ghaf.profiles.debug.enable [ cfg.user ]; - #services.userborn.enable = true; + + # Enable userborn + services.userborn = + { + enable = true; + } + // optionalAttrs cfg.enableLoginUser { + passwordFilesLocation = "/etc"; + }; }; } diff --git a/modules/givc/appvm.nix b/modules/givc/appvm.nix index 2274124de..7fdc109c2 100644 --- a/modules/givc/appvm.nix +++ b/modules/givc/appvm.nix @@ -44,7 +44,7 @@ in admin = config.ghaf.givc.adminConfig; }; - # Quick fix to allow linger (linger option in user def. currently doesn't work, e.g., bc mutable) - systemd.tmpfiles.rules = [ "f /var/lib/systemd/linger/${config.ghaf.users.accounts.user}" ]; + # Enable lingering + users.users.${config.ghaf.users.accounts.user}.linger = true; }; } diff --git a/modules/microvm/virtualization/microvm/adminvm.nix b/modules/microvm/virtualization/microvm/adminvm.nix index 32672ae2c..546ba5207 100644 --- a/modules/microvm/virtualization/microvm/adminvm.nix +++ b/modules/microvm/virtualization/microvm/adminvm.nix @@ -10,6 +10,7 @@ let adminvmBaseConfiguration = { imports = [ + inputs.impermanence.nixosModules.impermanence inputs.self.nixosModules.givc-adminvm (import ./common/vm-networking.nix { inherit @@ -20,6 +21,7 @@ let ; internalIP = 10; }) + ./common/storagevm.nix # We need to retrieve mac address and start log aggregator ../../../common/logging/hw-mac-retrieve.nix ../../../common/logging/logs-aggregator.nix @@ -27,7 +29,7 @@ let { lib, ... }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; development = { # NOTE: SSH port also becomes accessible on the network interface @@ -36,6 +38,8 @@ let debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + + # System systemd = { enable = true; withName = "adminvm-systemd"; @@ -47,10 +51,15 @@ let withDebug = configHost.ghaf.profiles.debug.enable; withHardenedConfigs = true; }; - givc.adminvm.enable = true; - # Log aggregation configuration + # Storage + storagevm = { + enable = true; + name = "adminvm"; + }; + + # Services logging = { client.enable = isLoggingEnabled; listener = { diff --git a/modules/microvm/virtualization/microvm/appvm.nix b/modules/microvm/virtualization/microvm/appvm.nix index 4445d2e89..d79168418 100644 --- a/modules/microvm/virtualization/microvm/appvm.nix +++ b/modules/microvm/virtualization/microvm/appvm.nix @@ -60,14 +60,15 @@ let in { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; - development = { ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + + # Systemd systemd = { enable = true; withName = "appvm-systemd"; @@ -80,11 +81,7 @@ let withHardenedConfigs = true; }; - ghaf-audio = { - inherit (vm.ghafAudio) enable; - name = "${vm.name}"; - }; - + # Storage storagevm = { enable = true; name = "${vm.name}"; @@ -98,7 +95,11 @@ let ]; }; - # Logging client configuration + # Services + ghaf-audio = { + inherit (vm.ghafAudio) enable; + name = "${vm.name}"; + }; logging.client.enable = configHost.ghaf.logging.client.enable; logging.client.endpoint = configHost.ghaf.logging.client.endpoint; }; diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix index f30e455ca..231990039 100644 --- a/modules/microvm/virtualization/microvm/audiovm.nix +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -39,14 +39,23 @@ let imports = [ ../../../common ]; ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; - development = { ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + users.accounts = { + enableProxyUser = true; + proxyuserGroups = [ + "audio" + "video" + "pipewire" + ]; + }; + + # System systemd = { enable = true; withName = "audiovm-systemd"; @@ -60,14 +69,18 @@ let withHardenedConfigs = true; }; givc.audiovm.enable = true; - services.audio.enable = true; - # Logging client configuration - logging.client.enable = configHost.ghaf.logging.client.enable; - logging.client.endpoint = configHost.ghaf.logging.client.endpoint; + + # Storage storagevm = { enable = true; name = "audiovm"; }; + + # Services + services.audio.enable = true; + # Logging client configuration + logging.client.enable = configHost.ghaf.logging.client.enable; + logging.client.endpoint = configHost.ghaf.logging.client.endpoint; }; environment = { @@ -78,17 +91,6 @@ let ] ++ lib.optional config.ghaf.development.debug.tools.enable pkgs.alsa-utils; }; - users.users."proxy-user-audio" = { - isNormalUser = true; - uid = config.ghaf.users.accounts.loginuid; - createHome = false; - extraGroups = [ - "audio" - "video" - "pipewire" - ]; - }; - time.timeZone = config.time.timeZone; system.stateVersion = lib.trivial.release; diff --git a/modules/microvm/virtualization/microvm/common/storagevm.nix b/modules/microvm/virtualization/microvm/common/storagevm.nix index aa431e23c..1df69d694 100644 --- a/modules/microvm/virtualization/microvm/common/storagevm.nix +++ b/modules/microvm/virtualization/microvm/common/storagevm.nix @@ -1,6 +1,10 @@ # Copyright 2022-2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ lib, config, ... }: +{ + lib, + config, + ... +}: let cfg = config.ghaf.storagevm; inherit (lib) @@ -8,11 +12,9 @@ let mkOption mkIf mkMerge - mkForce types optionals ; - mountPath = "/guestStorage"; in { options.ghaf.storagevm = { @@ -25,6 +27,14 @@ in type = types.str; }; + mountPath = mkOption { + description = '' + Mount path for the storage virtual machine. + ''; + type = types.str; + default = "/guestStorage"; + }; + directories = mkOption { # FIXME: Probably will lead to disgraceful error messages, as we # put typechecking on nix impermanence option. But other, @@ -70,7 +80,7 @@ in }; config = lib.mkIf cfg.enable { - fileSystems.${mountPath} = { + fileSystems.${cfg.mountPath} = { neededForBoot = true; options = [ "rw" @@ -79,7 +89,7 @@ in "noexec" ]; }; - virtualisation.fileSystems.${mountPath}.device = "/dev/vda"; + virtualisation.fileSystems.${cfg.mountPath}.device = "/dev/vda"; microvm.shares = [ { @@ -87,11 +97,11 @@ in proto = "virtiofs"; securityModel = "passthrough"; source = "/storagevm/${cfg.name}"; - mountPoint = mountPath; + mountPoint = cfg.mountPath; } ]; - environment.persistence.${mountPath} = lib.mkMerge [ + environment.persistence.${cfg.mountPath} = mkMerge [ { hideMounts = true; directories = @@ -99,11 +109,9 @@ in "/var/lib/nixos" ] ++ optionals config.ghaf.users.accounts.enableLoginUser [ - # TODO Replace with userborn setup "/etc" ]; - - files = [ + files = optionals (!config.ghaf.users.accounts.enableLoginUser) [ "/etc/ssh/ssh_host_ed25519_key.pub" "/etc/ssh/ssh_host_ed25519_key" ]; diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index 1b1ae856a..f567c0f48 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -33,25 +33,21 @@ let { lib, pkgs, ... }: { ghaf = { - users.accounts.enable = lib.mkDefault config.ghaf.users.accounts.enable; - users.accounts.enableLoginUser = true; + # Profiles profiles = { debug.enable = lib.mkDefault config.ghaf.profiles.debug.enable; applications.enable = false; graphics.enable = true; }; - - # To enable screen locking set to true - graphics.labwc = { - autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; - autologinUser = lib.mkDefault config.ghaf.graphics.labwc.autologinUser; - }; + users.accounts.enableLoginUser = true; development = { ssh.daemon.enable = lib.mkDefault config.ghaf.development.ssh.daemon.enable; debug.tools.enable = lib.mkDefault config.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault config.ghaf.development.nix-setup.enable; }; + + # System systemd = { enable = true; withName = "guivm-systemd"; @@ -63,9 +59,8 @@ let withHardenedConfigs = true; }; givc.guivm.enable = true; - # Logging client configuration - logging.client.enable = config.ghaf.logging.client.enable; - logging.client.endpoint = config.ghaf.logging.client.endpoint; + + # Storage storagevm = { enable = true; name = "guivm"; @@ -77,6 +72,15 @@ let "Videos" ]; }; + + # Services + # To enable screen locking set to true + graphics.labwc = { + autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; + autologinUser = lib.mkDefault config.ghaf.graphics.labwc.autologinUser; + }; + logging.client.enable = config.ghaf.logging.client.enable; + logging.client.endpoint = config.ghaf.logging.client.endpoint; services.disks.enable = true; services.disks.fileManager = "${pkgs.pcmanfm}/bin/pcmanfm"; services.xdghandlers.enable = true; @@ -91,10 +95,10 @@ let echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/waypipe-ssh/id_ed25519 -C "" chown ${config.ghaf.users.accounts.user}:${config.ghaf.users.accounts.user} /run/waypipe-ssh/* cp /run/waypipe-ssh/id_ed25519.pub /run/waypipe-ssh-public-key/id_ed25519.pub - echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/user-ssh/id_ed25519_net -C "proxy-user-network@net-vm" + echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/user-ssh/id_ed25519_net -C "proxyuser@net-vm" chown ${config.ghaf.users.accounts.loginuser}:${config.ghaf.users.accounts.loginuser} /run/user-ssh/* cp /run/user-ssh/id_ed25519_net.pub /run/waypipe-ssh-public-key/id_ed25519_net.pub - echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/user-ssh/id_ed25519_ad -C "proxy-user-audio@audio-vm" + echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/user-ssh/id_ed25519_ad -C "proxyuser@audio-vm" chown ${config.ghaf.users.accounts.loginuser}:${config.ghaf.users.accounts.loginuser} /run/user-ssh/* cp /run/user-ssh/id_ed25519_ad.pub /run/waypipe-ssh-public-key/id_ed25519_ad.pub ''; diff --git a/modules/microvm/virtualization/microvm/microvm-host.nix b/modules/microvm/virtualization/microvm/microvm-host.nix index ab9fcd09b..b383bd696 100644 --- a/modules/microvm/virtualization/microvm/microvm-host.nix +++ b/modules/microvm/virtualization/microvm/microvm-host.nix @@ -44,23 +44,26 @@ in (mkIf cfg.enable { microvm.host.enable = true; microvm.host.useNotifySockets = true; - ghaf.systemd = { - withName = "host-systemd"; - enable = true; - withAudit = config.ghaf.profiles.debug.enable; - withPolkit = true; - withTpm2Tss = pkgs.stdenv.hostPlatform.isx86; - withRepart = true; - withFido2 = true; - withCryptsetup = true; - withTimesyncd = cfg.networkSupport; - withNss = cfg.networkSupport; - withResolved = cfg.networkSupport; - withSerial = config.ghaf.profiles.debug.enable; - withDebug = config.ghaf.profiles.debug.enable; - withHardenedConfigs = true; + ghaf = { + # System + systemd = { + withName = "host-systemd"; + enable = true; + withAudit = config.ghaf.profiles.debug.enable; + withPolkit = true; + withTpm2Tss = pkgs.stdenv.hostPlatform.isx86; + withRepart = true; + withFido2 = true; + withCryptsetup = true; + withTimesyncd = cfg.networkSupport; + withNss = cfg.networkSupport; + withResolved = cfg.networkSupport; + withSerial = config.ghaf.profiles.debug.enable; + withDebug = config.ghaf.profiles.debug.enable; + withHardenedConfigs = true; + }; + givc.host.enable = true; }; - ghaf.givc.host.enable = true; # TODO: remove hardcoded paths systemd.services."microvm@audio-vm".serviceConfig = diff --git a/modules/microvm/virtualization/microvm/netvm.nix b/modules/microvm/virtualization/microvm/netvm.nix index 410156134..2cf9fb30a 100644 --- a/modules/microvm/virtualization/microvm/netvm.nix +++ b/modules/microvm/virtualization/microvm/netvm.nix @@ -43,7 +43,7 @@ let imports = [ ../../../common ]; ghaf = { - users.accounts.enable = lib.mkDefault config.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault config.ghaf.profiles.debug.enable; development = { # NOTE: SSH port also becomes accessible on the network interface @@ -52,6 +52,14 @@ let debug.tools.enable = lib.mkDefault config.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault config.ghaf.development.nix-setup.enable; }; + users.accounts = { + enableProxyUser = true; + proxyuserGroups = [ + "networkmanager" + ]; + }; + + # System systemd = { enable = true; withName = "netvm-systemd"; @@ -63,14 +71,19 @@ let withHardenedConfigs = true; }; givc.netvm.enable = true; - # Logging client configuration - logging.client.enable = config.ghaf.logging.client.enable; - logging.client.endpoint = config.ghaf.logging.client.endpoint; + + # Storage storagevm = { enable = true; name = "netvm"; directories = [ "/etc/NetworkManager/system-connections/" ]; }; + + # Services + # Logging client configuration + logging.client.enable = config.ghaf.logging.client.enable; + logging.client.endpoint = config.ghaf.logging.client.endpoint; + }; time.timeZone = config.time.timeZone; @@ -139,15 +152,6 @@ let ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir}.options = [ "ro" ]; }; - users.users."proxy-user-network" = { - isNormalUser = true; - createHome = false; - uid = config.ghaf.users.accounts.loginuid; - extraGroups = [ - "networkmanager" - ]; - }; - # SSH is very picky about to file permissions and ownership and will # accept neither direct path inside /nix/store or symlink that points # there. Therefore we copy the file to /etc/ssh/get-auth-keys (by diff --git a/packages/bt-launcher/default.nix b/packages/bt-launcher/default.nix index 706ece2b8..309a01139 100644 --- a/packages/bt-launcher/default.nix +++ b/packages/bt-launcher/default.nix @@ -17,7 +17,7 @@ writeShellApplication { export DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/ssh_session_dbus.sock export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/tmp/ssh_system_dbus.sock ${openssh}/bin/ssh -M -S /tmp/control_socket_bt \ - -f -N -q proxy-user-audio@audio-vm \ + -f -N -q proxyuser@audio-vm \ -i /run/user-ssh/id_ed25519_ad \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ @@ -27,7 +27,7 @@ writeShellApplication { -L /tmp/ssh_system_dbus.sock:/run/dbus/system_bus_socket # Use the control socket to close the ssh tunnel. close-tunnel() { - ${openssh}/bin/ssh -q -S /tmp/control_socket_bt -O exit proxy-user-audio@audio-vm + ${openssh}/bin/ssh -q -S /tmp/control_socket_bt -O exit proxyuser@audio-vm } launch-blueman() { diff --git a/packages/nm-launcher/default.nix b/packages/nm-launcher/default.nix index d433b388f..2186739fb 100644 --- a/packages/nm-launcher/default.nix +++ b/packages/nm-launcher/default.nix @@ -19,7 +19,7 @@ writeShellApplication { # export DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/ssh_session_dbus.sock export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/tmp/ssh_system_dbus.sock ${openssh}/bin/ssh -M -S /tmp/control_socket \ - -f -N -q proxy-user-network@net-vm \ + -f -N -q proxyuser@net-vm \ -i /run/user-ssh/id_ed25519_net \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ @@ -29,7 +29,7 @@ writeShellApplication { -L /tmp/ssh_system_dbus.sock:/run/dbus/system_bus_socket ${networkmanagerapplet}/bin/nm-applet --indicator # Use the control socket to close the ssh tunnel. - ${openssh}/bin/ssh -q -S /tmp/control_socket -O exit proxy-user-network@net-vm + ${openssh}/bin/ssh -q -S /tmp/control_socket -O exit proxyuser@net-vm ''; meta = {