From 8f410527d0dd97813162a43cc36c24fe1b9d5068 Mon Sep 17 00:00:00 2001 From: Manuel Bluhm Date: Wed, 11 Dec 2024 12:01:22 +0400 Subject: [PATCH] feat(user accounts): new ghaf user account setup - introducing userborn - disabling mutable users - re-factoring ghaf account to admin account - introducing login user account with homed + auxiliary accounts - impermanence flake input pinned to userborn patch - /etc/machine-id (gui-vm) is currently hardcoded as login user identity file depends on it. It should be generated on first boot and persistet. Workaround is available upstream (after userborn patch) in impermanence but does not seem to work with our setup, investigation required Signed-off-by: Manuel Bluhm --- flake.lock | 7 +- flake.nix | 2 +- modules/common/default.nix | 2 +- modules/common/profiles/debug.nix | 1 - modules/common/profiles/release.nix | 2 +- modules/common/services/audio.nix | 7 - modules/common/services/fprint.nix | 23 +- modules/common/services/xdgopener.nix | 4 +- modules/common/systemd/base.nix | 10 +- modules/common/users/README.md | 101 +++++++ modules/common/users/accounts.nix | 66 ----- modules/common/users/admin.nix | 82 ++++++ modules/common/users/common.nix | 26 ++ modules/common/users/default.nix | 9 + modules/common/users/desktop.nix | 248 ++++++++++++++++++ modules/common/users/other.nix | 111 ++++++++ modules/desktop/graphics/labwc.config.nix | 2 +- modules/desktop/graphics/labwc.nix | 2 +- modules/givc/appvm.nix | 4 +- modules/givc/audiovm.nix | 3 +- modules/givc/common.nix | 2 +- modules/givc/netvm.nix | 3 +- modules/hardware/common/shared-mem.nix | 24 +- .../virtualization/microvm/adminvm.nix | 14 +- .../microvm/virtualization/microvm/appvm.nix | 15 +- .../virtualization/microvm/audiovm.nix | 72 ++--- .../microvm/common/shared-directory.nix | 12 +- .../microvm/common/storagevm.nix | 52 +++- .../microvm/virtualization/microvm/guivm.nix | 96 +++---- .../virtualization/microvm/idsvm/idsvm.nix | 1 - .../virtualization/microvm/microvm-host.nix | 37 ++- .../microvm/virtualization/microvm/netvm.nix | 73 ++---- modules/reference/personalize/keys.nix | 2 +- packages/ghaf-powercontrol/default.nix | 2 +- packages/ghaf-xdg-open/default.nix | 12 +- packages/ssh-keys-helper/default.nix | 2 +- packages/wifi-signal-strength/default.nix | 6 +- 37 files changed, 816 insertions(+), 321 deletions(-) create mode 100644 modules/common/users/README.md delete mode 100644 modules/common/users/accounts.nix create mode 100644 modules/common/users/admin.nix create mode 100644 modules/common/users/common.nix create mode 100644 modules/common/users/default.nix create mode 100644 modules/common/users/desktop.nix create mode 100644 modules/common/users/other.nix diff --git a/flake.lock b/flake.lock index 26330790a2..15bb3e8cad 100644 --- a/flake.lock +++ b/flake.lock @@ -310,16 +310,17 @@ }, "impermanence": { "locked": { - "lastModified": 1731242966, - "narHash": "sha256-B3C3JLbGw0FtLSWCjBxU961gLNv+BOOBC6WvstKLYMw=", + "lastModified": 1728049659, + "narHash": "sha256-lGtad92Y/TnqpXRlZ1syiEq5czpvblKmcypeqGPiVF4=", "owner": "nix-community", "repo": "impermanence", - "rev": "3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" }, "original": { "owner": "nix-community", "repo": "impermanence", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" } }, diff --git a/flake.nix b/flake.nix index c30670bfac..952a5a684e 100644 --- a/flake.nix +++ b/flake.nix @@ -140,7 +140,7 @@ }; impermanence = { - url = "github:nix-community/impermanence"; + url = "github:nix-community/impermanence/32b1094d28d5fbedcc85a403bc08c8877b396255"; }; givc = { diff --git a/modules/common/default.nix b/modules/common/default.nix index 26011fa033..6ce4a0b3a8 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -11,7 +11,7 @@ ./firewall ./profiles ./security - ./users/accounts.nix + ./users ./version ./virtualization/docker.nix ./systemd diff --git a/modules/common/profiles/debug.nix b/modules/common/profiles/debug.nix index 21a5194f17..3b7020f649 100644 --- a/modules/common/profiles/debug.nix +++ b/modules/common/profiles/debug.nix @@ -15,7 +15,6 @@ in config = lib.mkIf cfg.enable { # Enable default accounts and passwords ghaf = { - users.accounts.enable = true; # Enable development on target development = { nix-setup.enable = true; diff --git a/modules/common/profiles/release.nix b/modules/common/profiles/release.nix index f06a6a72fc..056007f54d 100644 --- a/modules/common/profiles/release.nix +++ b/modules/common/profiles/release.nix @@ -18,6 +18,6 @@ in # TODO this needs to be refined when we define a policy for the # processes and the UID/groups that should be enabled by default # if not already covered by systemd - ghaf.users.accounts.enable = true; + # ghaf.users.admin.enable = true; }; } diff --git a/modules/common/services/audio.nix b/modules/common/services/audio.nix index 00d0e541a6..ff4cd36c78 100644 --- a/modules/common/services/audio.nix +++ b/modules/common/services/audio.nix @@ -81,13 +81,6 @@ in }; }; - # Allow ghaf user to access pulseaudio and pipewire - users.extraUsers.ghaf.extraGroups = [ - "audio" - "video" - "pipewire" - ]; - # Start pipewire on system boot systemd.services.pipewire.wantedBy = [ "multi-user.target" ]; diff --git a/modules/common/services/fprint.nix b/modules/common/services/fprint.nix index 87654b43f5..e13eabf052 100644 --- a/modules/common/services/fprint.nix +++ b/modules/common/services/fprint.nix @@ -46,38 +46,19 @@ in // Allow user to verify fingerprints polkit.addRule(function(action, subject) { if (action.id == "net.reactivated.fprint.device.verify" && - subject.user == "ghaf") { + subject.isInGroup ("users")) { return polkit.Result.YES; } }); // Allow user to enroll fingerprints polkit.addRule(function(action, subject) { if (action.id == "net.reactivated.fprint.device.enroll" && - subject.user == "ghaf") { + subject.isInGroup ("users")) { return polkit.Result.YES; } }); ''; }; - # PAM rules for swaylock fingerprint reader - pam.services = { - swaylock.text = '' - # Account management. - account required pam_unix.so - - # Authentication management. - auth sufficient pam_unix.so likeauth try_first_pass - auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so - auth required pam_deny.so - - # Password management. - password sufficient pam_unix.so nullok sha512 - - # Session management. - session required pam_env.so conffile=/etc/pam/environment readenv=0 - session required pam_unix.so - ''; - }; }; }; } diff --git a/modules/common/services/xdgopener.nix b/modules/common/services/xdgopener.nix index a7e7985d49..cc0b726b1e 100644 --- a/modules/common/services/xdgopener.nix +++ b/modules/common/services/xdgopener.nix @@ -22,6 +22,7 @@ let # into all targets ghaf-xdg-open = pkgs.callPackage ../../../packages/ghaf-xdg-open { inherit (config.ghaf.security.sshKeys) sshKeyPath; + user = config.ghaf.users.appUser.name; }; in { @@ -51,9 +52,6 @@ in services."xdg@" = { description = "XDG opener"; 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"; ExecStart = "${ghaf-xdg-open}/bin/ghaf-xdg-open"; StandardInput = "socket"; StandardOutput = "journal"; diff --git a/modules/common/systemd/base.nix b/modules/common/systemd/base.nix index 786542ca47..4a67069231 100644 --- a/modules/common/systemd/base.nix +++ b/modules/common/systemd/base.nix @@ -30,10 +30,11 @@ let inherit (cfg) withAudit; withCompression = true; withCoredump = cfg.withDebug || cfg.withMachines; - inherit (cfg) withCryptsetup; + withCryptsetup = cfg.withCryptsetup || cfg.withHomed; inherit (cfg) withEfi; inherit (cfg) withBootloader; inherit (cfg) withFido2; + inherit (cfg) withHomed; inherit (cfg) withHostnamed; withImportd = cfg.withMachines; withKexectools = cfg.withDebug; @@ -55,6 +56,7 @@ let inherit (cfg) withTimesyncd; inherit (cfg) withTpm2Tss; inherit (cfg) withUkify; + withUserDb = cfg.withHomed; withUtmp = cfg.withJournal || cfg.withAudit; } // lib.optionalAttrs (lib.strings.versionAtLeast pkgs.systemdMinimal.version "255.0") { @@ -230,6 +232,12 @@ in default = false; }; + withHomed = mkOption { + description = "Enable systemd homed for users home functionality."; + type = types.bool; + default = false; + }; + withHostnamed = mkOption { description = "Enable systemd hostname daemon."; type = types.bool; diff --git a/modules/common/users/README.md b/modules/common/users/README.md new file mode 100644 index 0000000000..0298088ac3 --- /dev/null +++ b/modules/common/users/README.md @@ -0,0 +1,101 @@ +# Ghaf user setup + +## Rationale +The changes address the separation between declarative and runtime +users. Declarative user definitions are appropriate if their +configuration should be the same across different machines. +Non-declarative users are introduced that allow changes without +re-building the configuration, and are purely device specific. + +For more consistent declarative user management, 'userborn' is +introduced, which increases consistency by managing user parameter +changes across re-builds. It is enabled for host and all VMs by default. +The NixOS configuration `users.mutableUsers` is set to false, thus +the generic user tools cannot be used at runtime (`passwd`, `useradd`, +etc.). + +To manage non-declarative users, 'systemd-homed' is used. It offers a +variety of concepts that are in line with our requirements such as +on-the-fly user creation, encryption, CIFS-integration, and more. + +## Code structure + +common.nix - common settings +admin.nix - admin account settings +desktop.nix - user accounts to manage desktop +other.nix - template to add other declarative users + +## Accounts +All accounts are available to be configured. Note that they are only +available in the respective VM where they are specifically enabled, with +exception for the admin account. + +### Admin account +The 'ghaf' user account is now the admin account (+wheel), and enabled by +default in host and all VMs. As this account is for administrative +purposes, it should not run a desktop session. This currently works with +some limitations, but at the moment we do not run a full-fletched multi-user +system. + +### Login/desktop user account +The 'loginUser' account can be enabled and sets up a non-declarative +user with a reserved UID. It is a self-contained account, that currently +runs the user desktop session. + +### Auxiliary accounts +Two auxiliary accounts are available that share the login users UID to +keep these UIDs consistent across VMs: + 1) Proxy user + This user is used in system VMs that provide services. These services + are currently accessible via the dbus proxy, and require the same UID. + 2) App user + This user is used in app VMs to run the user sessions (including applications). + The shared UID is helpful to map access rights across machines and support + legacy functionality. + +### Other (declarative/managed) user accounts +While any additional user accounts may be freely created and administered, a template +for configuration managed users is provided. + +## Future work +This patch introduces new account management on Ghaf. Based on the changes, +future work is required to extend it. + +### Extending login-user functionality +Currently, a minimalistic setup script runs on first GUI-VM boot. The login +user setup may be extended with: + + - Improvement of user creation script + - Graphical interface for user creation + - Feature integration: (supported by homed) + - CIFS/remote user storage integration + - FIDO token integration + - User ssh keys + - Avatar/Background/Locale/Timezone/Location + - External home support (e.g., USB or network storage) + - Potential re-work to run graphical session as static user + - Potential multi-user system with migratable data + +### Overall user account improvements + +- removing (hardcoded) ssh dependencies +- removing ssh root access, e.g., update user (PoC available) +- centralized mechanism (profiles) to administer declarative user account data (e.g., passwords) +- password policies mechanism, especially considering declarative users passwords are in nix store + +### User data implications +While the login user setup provides some containment, currently user data is still +spread across the system, such as + - Persistent user-related platform data (e.g., wifi passwords) + - Persistent user data in app VMs (e.g., browser profiles/passwords) + - File sharing between VMs + +Respective mechanisms are currently under investigation. + +## Implementation notes + + - VM storage shares have been re-named for consistency (-vm) + - impermanence flake input pinned to userborn patch + - /etc/machine-id (gui-vm) is currently hardcoded as login user identity file depends on it. + It should be generated on first boot and persistet. Workaround is available upstream (after + userborn patch) in impermanence but does not seem to work with our setup, investigation required diff --git a/modules/common/users/accounts.nix b/modules/common/users/accounts.nix deleted file mode 100644 index 01b24aa20c..0000000000 --- a/modules/common/users/accounts.nix +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors -# SPDX-License-Identifier: Apache-2.0 -{ config, lib, ... }: -# account for the development time login with sudo rights -let - cfg = config.ghaf.users.accounts; - inherit (lib) - mkEnableOption - mkOption - optionals - mkIf - types - ; -in -{ - #TODO Extend this to allow definition of multiple users - options.ghaf.users.accounts = { - enable = mkEnableOption "Default account Setup"; - user = mkOption { - default = "ghaf"; - type = with types; str; - description = '' - A default user to create in the system. - ''; - }; - uid = mkOption { - default = 1000; - type = with types; int; - description = '' - A default user id for the user. - ''; - }; - password = mkOption { - default = "ghaf"; - type = with types; str; - description = '' - A default password for the user. - ''; - }; - }; - - config = mkIf cfg.enable { - users = { - mutableUsers = false; - users."${cfg.user}" = { - isNormalUser = true; - inherit (cfg) password; - inherit (cfg) uid; - #TODO add "docker" use "lib.optionals" - extraGroups = [ - "wheel" - "video" - "networkmanager" - ] ++ optionals config.security.tpm2.enable [ "tss" ]; - }; - groups."${cfg.user}" = { - name = cfg.user; - members = [ cfg.user ]; - }; - }; - - # to build ghaf as ghaf-user with caches - nix.settings.trusted-users = mkIf config.ghaf.profiles.debug.enable [ cfg.user ]; - #services.userborn.enable = true; - }; -} diff --git a/modules/common/users/admin.nix b/modules/common/users/admin.nix new file mode 100644 index 0000000000..e03e2c32d3 --- /dev/null +++ b/modules/common/users/admin.nix @@ -0,0 +1,82 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + cfg = config.ghaf.users.admin; + inherit (lib) + mkIf + types + mkOption + optionals + ; +in +{ + options.ghaf.users.admin = { + enable = mkOption { + description = "Enable the admin user account. Enabled by default."; + type = types.bool; + default = true; + }; + name = mkOption { + description = "Admin account name."; + type = types.str; + default = "ghaf"; + }; + initialPassword = mkOption { + description = "Default password for the admin user account."; + type = types.str; + default = "ghaf"; + }; + initialHashedPassword = mkOption { + description = "Initial hashed password for the admin user account."; + type = types.nullOr types.str; + default = null; + }; + hashedPassword = mkOption { + description = "Hashed password for live updates."; + type = types.nullOr types.str; + default = null; + }; + extraGroups = mkOption { + description = "Extra groups for the admin user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + + config = mkIf cfg.enable { + + users = { + users = { + "${cfg.name}" = { + isNormalUser = true; + inherit (cfg) initialPassword; + inherit (cfg) initialHashedPassword; + inherit (cfg) hashedPassword; + home = "/tmp/${cfg.name}"; + extraGroups = + [ + "wheel" + "video" + ] + ++ cfg.extraGroups + ++ optionals config.security.tpm2.enable [ "tss" ] + ++ optionals config.ghaf.virtualization.docker.daemon.enable [ "docker" ]; + }; + }; + groups = { + "${cfg.name}" = { + inherit (cfg) name; + members = [ cfg.name ]; + }; + }; + }; + + # to build ghaf as admin with caches + nix.settings.trusted-users = mkIf config.ghaf.profiles.debug.enable [ cfg.name ]; + }; +} diff --git a/modules/common/users/common.nix b/modules/common/users/common.nix new file mode 100644 index 0000000000..65fbc96c98 --- /dev/null +++ b/modules/common/users/common.nix @@ -0,0 +1,26 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + inherit (lib) mkDefault hasAttr; + hasStorageVm = (hasAttr "storagevm" config.ghaf) && config.ghaf.storagevm.enable; +in +{ + # Common ghaf user settings + config = { + + # Disable mutable users + users.mutableUsers = mkDefault false; + + # Enable userborn + services.userborn = { + enable = mkDefault true; + passwordFilesLocation = if hasStorageVm then "/var/lib/nixos" else "/etc"; + }; + + }; +} diff --git a/modules/common/users/default.nix b/modules/common/users/default.nix new file mode 100644 index 0000000000..5338bf42c1 --- /dev/null +++ b/modules/common/users/default.nix @@ -0,0 +1,9 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + imports = [ + ./common.nix + ./admin.nix + ./desktop.nix + ]; +} diff --git a/modules/common/users/desktop.nix b/modules/common/users/desktop.nix new file mode 100644 index 0000000000..c635916c12 --- /dev/null +++ b/modules/common/users/desktop.nix @@ -0,0 +1,248 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.ghaf.users; + inherit (lib) + mkIf + types + mkOption + mkMerge + optionalString + concatStringsSep + ; + + loginUserAccount = types.submodule { + options = { + enable = mkOption { + description = "Enable desktop login user account."; + type = types.bool; + default = false; + }; + uid = mkOption { + description = '' + Login user identifier (uid). Defaults to 1001. + This UID is also used by the proxy and app auxiliary users. + ''; + type = types.int; + default = 1001; + }; + extraGroups = mkOption { + description = "Extra groups for the login user."; + type = types.listOf types.str; + default = [ ]; + }; + homeSize = mkOption { + description = '' + Size of the home directory for the login user in MB (integer). + The integer size is inherited from the microvm volume size parameter. + ''; + type = types.int; + default = 10000; + }; + }; + }; + + auxiliaryAccount = types.submodule { + options = { + enable = mkOption { + description = "Enable auxiliary user account."; + type = types.bool; + default = false; + }; + name = mkOption { + description = "Auxiliary user's name."; + type = types.str; + }; + extraGroups = mkOption { + description = "Extra groups for the auxiliary user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + }; + +in +{ + options.ghaf.users = { + # Main UI user + loginUser = mkOption { + description = "User account for desktop login."; + type = loginUserAccount; + default = { }; + }; + # Proxy user for dbus + proxyUser = mkOption { + description = "User account for dbus proxy functionality."; + type = auxiliaryAccount; + }; + # App user for running applications + appUser = mkOption { + description = "User account to run applications."; + type = auxiliaryAccount; + }; + }; + + config = mkMerge [ + { + assertions = [ + { + assertion = cfg.loginUser.enable -> config.ghaf.systemd.withHomed; + message = "You cannot enable login user without systemd-homed. Enable homed service in systemd module."; + } + { + assertion = cfg.loginUser.enable -> !cfg.proxyUser.enable; + message = "You cannot enable both login and proxy users at the same time."; + } + { + assertion = cfg.loginUser.enable -> !cfg.appUser.enable; + message = "You cannot enable both login and app users at the same time."; + } + ]; + + # Hardcode auxiliary user names + ghaf.users.appUser.name = "appuser"; + ghaf.users.proxyUser.name = "proxyuser"; + + users = { + users = mkMerge [ + (mkIf cfg.proxyUser.enable { + "${cfg.proxyUser.name}" = { + isNormalUser = true; + createHome = false; + inherit (cfg.loginUser) uid; + inherit (cfg.proxyUser) extraGroups; + }; + }) + (mkIf cfg.appUser.enable { + "${cfg.appUser.name}" = { + isNormalUser = true; + createHome = true; + inherit (cfg.loginUser) uid; + inherit (cfg.appUser) extraGroups; + }; + }) + ]; + groups = mkMerge [ + (mkIf cfg.proxyUser.enable { + "${cfg.proxyUser.name}" = { + inherit (cfg.proxyUser) name; + members = [ cfg.proxyUser.name ]; + }; + }) + (mkIf cfg.appUser.enable { + "${cfg.appUser.name}" = { + inherit (cfg.appUser) name; + members = [ cfg.appUser.name ]; + }; + }) + ]; + }; + } + + # Login user setup with homed + (mkIf cfg.loginUser.enable { + + # Enable homed service + services.homed.enable = true; + + # First boot login user setup service + systemd.services.ghaf-loginuser-setup = + let + userSetupScript = pkgs.writeShellApplication { + name = "ghaf-user-setup"; + runtimeInputs = [ + pkgs.coreutils + pkgs.ncurses + pkgs.brightnessctl + ]; + text = '' + brightnessctl set 100% + clear + echo -e "\e[1;32;1mWelcome to Ghaf \e[0m" + echo "" + echo "Start by creating your user account." + echo "" + + # Read new user name + ACCEPTABLE_USER=false + until $ACCEPTABLE_USER; do + echo -n "Enter your user name: " + read -e -r USERNAME + USERNAME=''${USERNAME//_/} + USERNAME=''${USERNAME// /_} + USERNAME=''${USERNAME//[^a-zA-Z0-9_]/} + USERNAME=''$(echo -n "$USERNAME" | tr '[:upper:]' '[:lower:]') + if grep -q -w "$USERNAME:" /etc/passwd; then + echo "User $USERNAME already exists. Please choose another user name." + else + ACCEPTABLE_USER=true + fi + done + + echo "" + echo -n "Enter your full name: " + read -e -r REALNAME + REALNAME=''${REALNAME//[^a-zA-Z ]/} + [[ -n "$REALNAME" ]] || REALNAME="$USERNAME"; + + echo "" + echo "Setting up your user account and creating encrypted home folder after you enter your password." + echo "This may take a while..." + echo "" + + # Add login user and home + homectl create "$USERNAME" \ + --real-name="$REALNAME" \ + --skel=/etc/skel \ + --storage=luks \ + --luks-pbkdf-type=argon2id \ + --enforce-password-policy=true \ + --drop-caches=true \ + --nosuid=true \ + --noexec=true \ + --nodev=true \ + --disk-size=${toString cfg.loginUser.homeSize}M \ + --shell=/run/current-system/sw/bin/bash \ + --uid=${toString cfg.loginUser.uid} \ + --member-of=users${ + optionalString ( + cfg.loginUser.extraGroups != [ ] + ) ",${concatStringsSep "," cfg.loginUser.extraGroups}" + } + + # Lock user creation script + install -m 000 /dev/null /var/lib/nixos/user.lock + + echo "" + echo "User $USERNAME created. Starting user session..." + sleep 1 + ''; + }; + in + { + description = "First boot user setup"; + enable = true; + requiredBy = [ "multi-user.target" ]; + before = [ "systemd-user-sessions.service" ]; + path = [ userSetupScript ]; + unitConfig.ConditionPathExists = "!/var/lib/nixos/user.lock"; + serviceConfig = { + Type = "oneshot"; + StandardInput = "tty"; + StandardOutput = "tty"; + StandardError = "journal"; + TTYPath = "/dev/tty1"; + TTYReset = true; + TTYVHangup = true; + ExecStart = "${userSetupScript}/bin/ghaf-user-setup"; + }; + }; + }) + ]; +} diff --git a/modules/common/users/other.nix b/modules/common/users/other.nix new file mode 100644 index 0000000000..db2848ab40 --- /dev/null +++ b/modules/common/users/other.nix @@ -0,0 +1,111 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + cfg = config.ghaf.users; + inherit (lib) + mkIf + types + mkOption + ; + + userAccount = types.submodule { + options = { + enable = mkOption { + description = "Enable user account"; + type = types.bool; + default = false; + }; + name = mkOption { + description = "User name"; + type = types.str; + default = ""; + }; + initialPassword = mkOption { + description = "Default password for the admin user account."; + type = types.str; + default = "ghaf"; + }; + initialHashedPassword = mkOption { + description = "Initial hashed password for the admin user account."; + type = types.nullOr types.str; + default = null; + }; + hashedPassword = mkOption { + description = "Hashed password for live updates."; + type = types.nullOr types.str; + default = null; + }; + uid = mkOption { + description = "Optional user identifier (uid). Defaults to null."; + type = types.nullOr types.int; + default = null; + }; + gid = mkOption { + description = "Optional primary group identifier (gid). Defaults to null."; + type = types.nullOr types.int; + default = null; + }; + extraGroups = mkOption { + description = "Extra groups for the user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + }; + +in +{ + options.ghaf.users = { + managed = mkOption { + description = '' + List of declarativively managed user accounts. + + The ghaf user interface for declarative users has the following options: + - enable: Enable user account + - name: User name + - initialPassword: Default password for the user account. + - initialHashedPassword: Initial hashed password for the user account. + - hashedPassword: Hashed password for live updates. + - uid: Optional user identifier (uid). Defaults to null. + - gid: Optional primary group identifier (gid). Defaults to null. + - extraGroups: Extra groups for the user. + + Additional user options may be handled through the NixOS user module. + ''; + type = types.listOf userAccount; + default = [ ]; + }; + }; + + config = { + users = { + users = { + "${cfg.name}" = + { + isNormalUser = true; + inherit (cfg) initialPassword; + inherit (cfg) initialHashedPassword; + inherit (cfg) hashedPassword; + inherit (cfg) extraGroups; + } + // lib.optionalAttrs (cfg.uid != null) { + inherit (cfg) uid; + } + // lib.optionalAttrs (cfg.gid != null) { + inherit (cfg) gid; + }; + groups = mkIf (cfg.gid == null) { + "${cfg.name}" = { + inherit (cfg) name; + members = [ cfg.name ]; + }; + }; + }; + }; + }; +} diff --git a/modules/desktop/graphics/labwc.config.nix b/modules/desktop/graphics/labwc.config.nix index 5848ba02c0..85151cec64 100644 --- a/modules/desktop/graphics/labwc.config.nix +++ b/modules/desktop/graphics/labwc.config.nix @@ -342,7 +342,7 @@ in services.greetd.settings = { initial_session = lib.mkIf (cfg.autologinUser != null) { - user = "ghaf"; + user = config.ghaf.users.admin.name; command = "ghaf-session"; }; }; diff --git a/modules/desktop/graphics/labwc.nix b/modules/desktop/graphics/labwc.nix index 02402bc208..c228686be0 100644 --- a/modules/desktop/graphics/labwc.nix +++ b/modules/desktop/graphics/labwc.nix @@ -26,7 +26,7 @@ in }; autologinUser = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = config.ghaf.users.accounts.user; + default = config.ghaf.users.admin.name; description = '' Username of the account that will be automatically logged in to the desktop. If unspecified, the login manager is shown as usual. diff --git a/modules/givc/appvm.nix b/modules/givc/appvm.nix index f0e698e27b..c15e6a0015 100644 --- a/modules/givc/appvm.nix +++ b/modules/givc/appvm.nix @@ -47,7 +47,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.appUser.name}.linger = true; }; } diff --git a/modules/givc/audiovm.nix b/modules/givc/audiovm.nix index a048f5c268..9accd06bc9 100644 --- a/modules/givc/audiovm.nix +++ b/modules/givc/audiovm.nix @@ -42,8 +42,7 @@ in enable = true; system = { enable = true; - # TODO Change this with new user setup - user = "ghaf"; + user = config.ghaf.users.proxyUser.name; socket = "/tmp/dbusproxy_snd.sock"; policy = { talk = [ diff --git a/modules/givc/common.nix b/modules/givc/common.nix index a7b929f466..0b8e36f249 100644 --- a/modules/givc/common.nix +++ b/modules/givc/common.nix @@ -12,7 +12,7 @@ let mitmEnabled = config.ghaf.virtualization.microvm.idsvm.enable && config.ghaf.virtualization.microvm.idsvm.mitmproxy.enable; - mitmExtraArgs = lib.optionalString mitmEnabled "--user-data-dir=/home/${config.ghaf.users.accounts.user}/.config/google-chrome/Default --test-type --ignore-certificate-errors-spki-list=Bq49YmAq1CG6FuBzp8nsyRXumW7Dmkp7QQ/F82azxGU="; + mitmExtraArgs = lib.optionalString mitmEnabled "--user-data-dir=/home/${config.ghaf.users.appUser.name}/.config/google-chrome/Default --test-type --ignore-certificate-errors-spki-list=Bq49YmAq1CG6FuBzp8nsyRXumW7Dmkp7QQ/F82azxGU="; in { options.ghaf.givc = { diff --git a/modules/givc/netvm.nix b/modules/givc/netvm.nix index a1a7cf1d12..b760e76ec6 100644 --- a/modules/givc/netvm.nix +++ b/modules/givc/netvm.nix @@ -50,8 +50,7 @@ in enable = true; system = { enable = true; - # TODO Change this with new user setup - user = "ghaf"; + user = config.ghaf.users.proxyUser.name; socket = "/tmp/dbusproxy_net.sock"; policy = { own = [ diff --git a/modules/hardware/common/shared-mem.nix b/modules/hardware/common/shared-mem.nix index 2228949756..c8c7520dd0 100644 --- a/modules/hardware/common/shared-mem.nix +++ b/modules/hardware/common/shared-mem.nix @@ -34,7 +34,7 @@ in type = types.int; default = 16; description = mdDoc '' - Specifies the size of the shared memory region, measured in + Specifies the size of the shared memory region, measured in megabytes (MB) ''; }; @@ -42,7 +42,7 @@ in type = types.str; default = "2M"; description = mdDoc '' - Specifies the size of the large memory page area. Supported kernel + Specifies the size of the large memory page area. Supported kernel values are 2 MB and 1 GB ''; apply = @@ -56,7 +56,7 @@ in type = types.path; default = "/tmp/ivshmem_socket"; # The value is hardcoded in the application description = mdDoc '' - Specifies the path to the shared memory socket, used by QEMU + Specifies the path to the shared memory socket, used by QEMU instances for inter-VM memory sharing and interrupt signaling ''; }; @@ -65,7 +65,7 @@ in default = "0x920000000"; description = mdDoc '' Maps the shared memory to a physical address if set to a non-zero value. - The address must be platform-specific and arbitrarily chosen to avoid + The address must be platform-specific and arbitrarily chosen to avoid conflicts with other memory areas, such as PCI regions. ''; }; @@ -93,19 +93,19 @@ in }; serverSocketPath = mkOption { type = types.path; - default = "/run/user/${builtins.toString config.ghaf.users.accounts.uid}/memsocket-server.sock"; + default = "/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/memsocket-server.sock"; description = mdDoc '' - Specifies the path of the listening socket, which is used by Waypipe - or other server applications as the output socket in server mode for + Specifies the path of the listening socket, which is used by Waypipe + or other server applications as the output socket in server mode for data transmission ''; }; clientSocketPath = mkOption { type = types.path; - default = "/run/user/${builtins.toString config.ghaf.users.accounts.uid}/memsocket-client.sock"; + default = "/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/memsocket-client.sock"; description = mdDoc '' - Specifies the location of the output socket, which will connected to - in order to receive data from AppVMs. This socket must be created by + Specifies the location of the output socket, which will connected to + in order to receive data from AppVMs. This socket must be created by another application, such as Waypipe, when operating in client mode ''; }; @@ -113,8 +113,8 @@ in type = types.bool; default = false; description = mdDoc '' - Enables the use of shared memory with Waypipe for Wayland-enabled - applications running on virtual machines (VMs), facilitating + Enables the use of shared memory with Waypipe for Wayland-enabled + applications running on virtual machines (VMs), facilitating efficient inter-VM communication ''; }; diff --git a/modules/microvm/virtualization/microvm/adminvm.nix b/modules/microvm/virtualization/microvm/adminvm.nix index 9e235a9ca1..8fdee3b585 100644 --- a/modules/microvm/virtualization/microvm/adminvm.nix +++ b/modules/microvm/virtualization/microvm/adminvm.nix @@ -21,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 @@ -29,7 +30,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 @@ -38,6 +39,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"; @@ -49,18 +52,19 @@ let withDebug = configHost.ghaf.profiles.debug.enable; withHardenedConfigs = true; }; + givc.adminvm.enable = true; + + # Storage storagevm = { enable = true; - name = "adminvm"; + name = vmName; files = [ "/etc/locale-givc.conf" "/etc/timezone.conf" ]; }; - givc.adminvm.enable = true; - - # Log aggregation configuration + # Services logging = { client.enable = isLoggingEnabled; listener = { diff --git a/modules/microvm/virtualization/microvm/appvm.nix b/modules/microvm/virtualization/microvm/appvm.nix index 6e912a174f..5e2b77d451 100644 --- a/modules/microvm/virtualization/microvm/appvm.nix +++ b/modules/microvm/virtualization/microvm/appvm.nix @@ -77,14 +77,17 @@ let }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; - profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; + # Profiles + users.appUser.enable = true; + 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"; @@ -106,8 +109,8 @@ let storagevm = { enable = true; - name = "${vm.name}"; - users.${config.ghaf.users.accounts.user}.directories = [ + name = vmName; + users.${config.ghaf.users.appUser.name}.directories = [ ".config/" "Downloads" "Music" @@ -130,7 +133,9 @@ let # setting mode), instead of symlinking it. environment.etc.${configHost.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - services.openssh = configHost.ghaf.security.sshKeys.sshAuthorizedKeysCommand; + services.openssh = configHost.ghaf.security.sshKeys.sshAuthorizedKeysCommand // { + authorizedKeysCommandUser = config.ghaf.users.appUser.name; + }; system.stateVersion = lib.trivial.release; diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix index 8918fd6b62..cefb803dfb 100644 --- a/modules/microvm/virtualization/microvm/audiovm.nix +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -4,21 +4,14 @@ { config, lib, - pkgs, ... }: let configHost = config; vmName = "audio-vm"; macAddress = "02:00:00:03:03:03"; - isGuiVmEnabled = config.ghaf.virtualization.microvm.guivm.enable; has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; - sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { - inherit pkgs; - inherit config; - }; - audiovmBaseConfiguration = { imports = [ inputs.self.nixosModules.givc-audiovm @@ -40,14 +33,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.proxyUser = { + enable = true; + extraGroups = [ + "audio" + "video" + "pipewire" + ]; + }; + + # System systemd = { enable = true; withName = "audiovm-systemd"; @@ -61,14 +63,18 @@ let withHardenedConfigs = true; }; givc.audiovm.enable = true; + + # Storage + storagevm = { + enable = true; + name = vmName; + }; + + # Services services.audio.enable = true; # Logging client configuration logging.client.enable = configHost.ghaf.logging.client.enable; logging.client.endpoint = configHost.ghaf.logging.client.endpoint; - storagevm = { - enable = true; - name = "audiovm"; - }; }; environment = { @@ -87,32 +93,20 @@ let hostPlatform.system = configHost.nixpkgs.hostPlatform.system; }; - services.openssh = config.ghaf.security.sshKeys.sshAuthorizedKeysCommand; - microvm = { # Optimize is disabled because when it is enabled, qemu is built without libusb optimize.enable = false; vcpu = 2; mem = 384; hypervisor = "qemu"; - shares = - [ - { - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "virtiofs"; - } - ] - ++ lib.optionals isGuiVmEnabled [ - { - # Add the waypipe-ssh public key to the microvm - tag = config.ghaf.security.sshKeys.waypipeSshPublicKeyName; - source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - proto = "virtiofs"; - } - ]; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "virtiofs"; + } + ]; writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; qemu = { machine = @@ -133,18 +127,6 @@ let ]; }; }; - - fileSystems = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir}.options = [ "ro" ]; - }; - - # 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 - # setting mode), instead of symlinking it. - environment.etc = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - }; } ) ]; diff --git a/modules/microvm/virtualization/microvm/common/shared-directory.nix b/modules/microvm/virtualization/microvm/common/shared-directory.nix index 80db4d6684..bb995d549d 100644 --- a/modules/microvm/virtualization/microvm/common/shared-directory.nix +++ b/modules/microvm/virtualization/microvm/common/shared-directory.nix @@ -4,10 +4,9 @@ name: { lib, config, ... }: let cfg = config.ghaf.storagevm; - shared-mountPath = "/tmp/shared/shares"; - inherit (config.ghaf.users.accounts) user; isGuiVm = builtins.stringLength name == 0; - userDir = "/home/${user}" + (if isGuiVm then "/Shares" else "/Unsafe\ share"); + shared-mountPath = "/tmp/shared/shares"; + userDir = if isGuiVm then "/Shares" else "/home/${config.ghaf.users.appUser.name}/Unsafe\ share"; in { config = lib.mkIf cfg.enable { @@ -37,5 +36,12 @@ in "x-gvfs-hide" ]; }; + + # Add bookmark to skel + environment.etc = lib.mkIf config.ghaf.users.loginUser.enable { + "skel/.gtk-bookmarks".text = '' + file:///Shares Shares + ''; + }; }; } diff --git a/modules/microvm/virtualization/microvm/common/storagevm.nix b/modules/microvm/virtualization/microvm/common/storagevm.nix index 15e3b84e0d..d39698156b 100644 --- a/modules/microvm/virtualization/microvm/common/storagevm.nix +++ b/modules/microvm/virtualization/microvm/common/storagevm.nix @@ -1,12 +1,22 @@ # Copyright 2022-2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ lib, config, ... }: +{ + lib, + config, + ... +}: let cfg = config.ghaf.storagevm; - mountPath = "/guestStorage"; + inherit (lib) + mkEnableOption + mkOption + mkIf + mkMerge + types + ; in { - options.ghaf.storagevm = with lib; { + options.ghaf.storagevm = { enable = mkEnableOption "StorageVM support"; name = mkOption { @@ -16,6 +26,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, @@ -61,7 +79,7 @@ in }; config = lib.mkIf cfg.enable { - fileSystems.${mountPath} = { + fileSystems.${cfg.mountPath} = { neededForBoot = true; options = [ "rw" @@ -70,7 +88,7 @@ in "noexec" ]; }; - virtualisation.fileSystems.${mountPath}.device = "/dev/vda"; + virtualisation.fileSystems.${cfg.mountPath}.device = "/dev/vda"; microvm.shares = [ { @@ -78,23 +96,41 @@ in proto = "virtiofs"; securityModel = "passthrough"; source = "/storagevm/${cfg.name}"; - mountPoint = mountPath; + mountPoint = cfg.mountPath; } ]; - environment.persistence.${mountPath} = lib.mkMerge [ + microvm.volumes = lib.optionals config.ghaf.users.loginUser.enable [ + { + image = "/storagevm/homes/${cfg.name}-home.img"; + size = builtins.floor (config.ghaf.users.loginUser.homeSize * 1.15); + fsType = "btrfs"; + mountPoint = "/home"; + } + ]; + + environment.persistence.${cfg.mountPath} = mkMerge [ { hideMounts = true; directories = [ "/var/lib/nixos" ]; - files = [ "/etc/ssh/ssh_host_ed25519_key.pub" "/etc/ssh/ssh_host_ed25519_key" ]; } { inherit (cfg) directories users files; } + (mkIf config.ghaf.users.loginUser.enable { + directories = [ + "/var/lib/systemd/home" + ]; + }) ]; + + # Workaround, fixes homed machine-id dependency + environment.etc = lib.optionalAttrs config.ghaf.users.loginUser.enable { + machine-id.text = "d8dee68f8d334c79ac8f8229921e0b25"; + }; }; } diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index 9dd1759d57..468a885e08 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -66,24 +66,17 @@ let in { ghaf = { - users.accounts.enable = lib.mkDefault config.ghaf.users.accounts.enable; + # Profiles profiles = { debug.enable = lib.mkDefault config.ghaf.profiles.debug.enable; applications.enable = false; graphics.enable = true; }; - - # Create launchers for regular apps running in the GUIVM and virtualized ones if GIVC is enabled - graphics.launchers = guivmLaunchers ++ lib.optionals config.ghaf.givc.enable virtualLaunchers; - - # 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; - securityContext = map (vm: { - identifier = vm.name; - color = vm.borderColor; - }) config.ghaf.virtualization.microvm.appvm.vms; + users = { + loginUser = { + enable = true; + extraGroups = [ "video" ]; + }; }; development = { @@ -91,40 +84,42 @@ let 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"; withAudit = config.ghaf.profiles.debug.enable; + withHomed = true; withLocaled = true; withNss = true; withResolved = true; withTimesyncd = true; withDebug = config.ghaf.profiles.debug.enable; - withHardenedConfigs = true; + withHardenedConfigs = false; }; 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"; - directories = [ - { - directory = "/var/lib/private/ollama"; - inherit (config.ghaf.users.accounts) user; - group = "ollama"; - mode = "u=rwx,g=,o="; - } - ]; - users.${config.ghaf.users.accounts.user}.directories = [ - ".cache" - ".config" - ".local" - "Pictures" - "Videos" - ]; + name = vmName; + }; + + # Services + + # Create launchers for regular apps running in the GUIVM and virtualized ones if GIVC is enabled + graphics.launchers = guivmLaunchers ++ lib.optionals config.ghaf.givc.enable virtualLaunchers; + graphics.labwc = { + autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; + autologinUser = lib.mkDefault config.ghaf.graphics.labwc.autologinUser; + securityContext = map (vm: { + identifier = vm.name; + color = vm.borderColor; + }) config.ghaf.virtualization.microvm.appvm.vms; }; + 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; @@ -141,7 +136,7 @@ let # Switch off display, if wayland is running if ${pkgs.procps}/bin/pgrep -fl "wayland" > /dev/null; then wl_running=1 - WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.accounts.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --off '*' + WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --off '*' else wl_running=0 fi @@ -151,7 +146,7 @@ let # Enable display if [ "$wl_running" -eq 1 ]; then - WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.accounts.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --on '*' + WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --on '*' fi ;; "button/lid LID open") @@ -163,12 +158,15 @@ let systemd.services."waypipe-ssh-keygen" = let + uid = "${toString config.ghaf.users.loginUser.uid}"; + pubDir = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; keygenScript = pkgs.writeShellScriptBin "waypipe-ssh-keygen" '' set -xeuo pipefail mkdir -p /run/waypipe-ssh echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/waypipe-ssh/id_ed25519 -C "" - chown ghaf:ghaf /run/waypipe-ssh/* - cp /run/waypipe-ssh/id_ed25519.pub /run/waypipe-ssh-public-key/id_ed25519.pub + chown ${uid}:users /run/waypipe-ssh/* + cp /run/waypipe-ssh/id_ed25519.pub ${pubDir}/id_ed25519.pub + chown -R ${uid}:users ${pubDir} ''; in { @@ -234,7 +232,7 @@ let hypervisor = "qemu"; shares = [ { - tag = "rw-waypipe-ssh-public-key"; + tag = "waypipe-ssh-public-key"; source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; proto = "virtiofs"; @@ -382,27 +380,5 @@ in imports = guivmBaseConfiguration.imports ++ cfg.extraModules; }; }; - - # This directory needs to be created before any of the microvms start. - systemd.services."create-waypipe-ssh-public-key-directory" = - let - script = pkgs.writeShellScriptBin "create-waypipe-ssh-public-key-directory" '' - mkdir -pv ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir} - chown -v microvm ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir} - ''; - in - { - enable = true; - description = "Create shared directory on host"; - path = [ ]; - wantedBy = [ "microvms.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardOutput = "journal"; - StandardError = "journal"; - ExecStart = "${script}/bin/create-waypipe-ssh-public-key-directory"; - }; - }; }; } diff --git a/modules/microvm/virtualization/microvm/idsvm/idsvm.nix b/modules/microvm/virtualization/microvm/idsvm/idsvm.nix index 4354e26c00..ab868988a1 100644 --- a/modules/microvm/virtualization/microvm/idsvm/idsvm.nix +++ b/modules/microvm/virtualization/microvm/idsvm/idsvm.nix @@ -25,7 +25,6 @@ let { lib, ... }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; virtualization.microvm.idsvm.mitmproxy.enable = diff --git a/modules/microvm/virtualization/microvm/microvm-host.nix b/modules/microvm/virtualization/microvm/microvm-host.nix index e0a129a430..e7e30203b6 100644 --- a/modules/microvm/virtualization/microvm/microvm-host.nix +++ b/modules/microvm/virtualization/microvm/microvm-host.nix @@ -9,6 +9,14 @@ }: let cfg = config.ghaf.virtualization.microvm-host; + inherit (lib) + mkEnableOption + mkOption + mkIf + mkMerge + types + ; + has_remove_pci_device = config.ghaf.hardware.definition.audio.removePciDevice != null; has_rescan_pci_device = config.ghaf.hardware.definition.audio.rescanPciDevice != null; has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; @@ -17,6 +25,7 @@ let config.ghaf.hardware.definition.audio.rescanPciDevice else config.ghaf.hardware.definition.audio.removePciDevice; + in { imports = [ @@ -24,27 +33,28 @@ in inputs.self.nixosModules.givc-host ]; options.ghaf.virtualization.microvm-host = { - enable = lib.mkEnableOption "MicroVM Host"; - networkSupport = lib.mkEnableOption "Network support services to run host applications."; + enable = mkEnableOption "MicroVM Host"; + networkSupport = mkEnableOption "Network support services to run host applications."; sharedVmDirectory = { - enable = lib.mkEnableOption "shared directory" // { + enable = mkEnableOption "shared directory" // { default = true; }; - vms = lib.mkOption { + vms = mkOption { description = '' List of names of virtual machines for which unsafe shared folder will be enabled. ''; - type = lib.types.listOf lib.types.str; + type = types.listOf types.str; default = [ ]; }; }; }; - config = lib.mkMerge [ - (lib.mkIf cfg.enable { + config = mkMerge [ + (mkIf cfg.enable { microvm.host.enable = true; microvm.host.useNotifySockets = true; + ghaf.systemd = { withName = "host-systemd"; enable = true; @@ -90,7 +100,7 @@ in }; }) - (lib.mkIf cfg.sharedVmDirectory.enable { + (mkIf cfg.sharedVmDirectory.enable { ghaf.virtualization.microvm.guivm.extraModules = [ (import ./common/shared-directory.nix "") ]; # Create directories required for sharing files with correct permissions. @@ -98,15 +108,20 @@ in let vmDirs = map ( n: - "d /storagevm/shared/shares/Unsafe\\x20${n}\\x20share/ 0700 ${config.ghaf.users.accounts.user} users" + "d /storagevm/shared/shares/Unsafe\\x20${n}\\x20share/ 0760 ${toString config.ghaf.users.loginUser.uid} users" ) cfg.sharedVmDirectory.vms; in [ "d /storagevm/shared 0755 root root" - "d /storagevm/shared/shares 0700 ${config.ghaf.users.accounts.user} users" + "d /storagevm/shared/shares 0760 ${toString config.ghaf.users.loginUser.uid} users" ] ++ vmDirs; - }) + { + # Add host directory for persistent home images + systemd.tmpfiles.rules = [ + "d /storagevm/homes 0770 microvm kvm -" + ]; + } ]; } diff --git a/modules/microvm/virtualization/microvm/netvm.nix b/modules/microvm/virtualization/microvm/netvm.nix index 9fa2830a5f..e5c0b65001 100644 --- a/modules/microvm/virtualization/microvm/netvm.nix +++ b/modules/microvm/virtualization/microvm/netvm.nix @@ -4,20 +4,12 @@ { config, lib, - pkgs, ... }: let vmName = "net-vm"; macAddress = "02:00:00:01:01:01"; - isGuiVmEnabled = config.ghaf.virtualization.microvm.guivm.enable; - - sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { - inherit pkgs; - inherit config; - }; - netvmBaseConfiguration = { imports = [ inputs.impermanence.nixosModules.impermanence @@ -43,7 +35,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 +44,16 @@ let debug.tools.enable = lib.mkDefault config.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault config.ghaf.development.nix-setup.enable; }; + users = { + proxyUser = { + enable = true; + extraGroups = [ + "networkmanager" + ]; + }; + }; + + # System systemd = { enable = true; withName = "netvm-systemd"; @@ -63,14 +65,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"; + name = vmName; 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; @@ -86,8 +93,6 @@ let firewall.allowedUDPPorts = [ 53 ]; }; - services.openssh = config.ghaf.security.sshKeys.sshAuthorizedKeysCommand; - # WORKAROUND: Create a rule to temporary hardcode device name for Wi-Fi adapter on x86 # TODO this is a dirty hack to guard against adding this to Nvidia/vm targets which # dont have that definition structure yet defined. FIXME. @@ -100,24 +105,14 @@ let # Optimize is disabled because when it is enabled, qemu is built without libusb optimize.enable = false; hypervisor = "qemu"; - shares = - [ - { - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "virtiofs"; - } - ] - ++ lib.optionals isGuiVmEnabled [ - { - # Add the waypipe-ssh public key to the microvm - tag = config.ghaf.security.sshKeys.waypipeSshPublicKeyName; - source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - proto = "virtiofs"; - } - ]; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "virtiofs"; + } + ]; writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; qemu = { @@ -134,18 +129,6 @@ let ]; }; }; - - fileSystems = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir}.options = [ "ro" ]; - }; - - # 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 - # setting mode), instead of symlinking it. - environment.etc = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - }; } ) ]; diff --git a/modules/reference/personalize/keys.nix b/modules/reference/personalize/keys.nix index 6058c22636..b3e23e19d8 100644 --- a/modules/reference/personalize/keys.nix +++ b/modules/reference/personalize/keys.nix @@ -31,7 +31,7 @@ in config = mkIf cfg.enable { users.users.root.openssh.authorizedKeys.keys = authorizedSshKeys; - users.users.${config.ghaf.users.accounts.user}.openssh.authorizedKeys.keys = authorizedSshKeys; + users.users.${config.ghaf.users.admin.name}.openssh.authorizedKeys.keys = authorizedSshKeys; ghaf.services.yubikey.u2fKeys = mkForce (concatStrings authorizedYubikeys); }; } diff --git a/packages/ghaf-powercontrol/default.nix b/packages/ghaf-powercontrol/default.nix index bd008830a8..dfc4ba4741 100644 --- a/packages/ghaf-powercontrol/default.nix +++ b/packages/ghaf-powercontrol/default.nix @@ -24,7 +24,7 @@ let useGivc = ghafConfig.givc.enable; # Handle Wayland display power state waylandDisplayCmd = command: '' - WAYLAND_DISPLAY=/run/user/${builtins.toString ghafConfig.users.accounts.uid}/wayland-0 \ + WAYLAND_DISPLAY=/run/user/${builtins.toString ghafConfig.users.loginUser.uid}/wayland-0 \ wlopm --${command} '*' ''; in diff --git a/packages/ghaf-xdg-open/default.nix b/packages/ghaf-xdg-open/default.nix index 47ad36850f..d500094ca9 100644 --- a/packages/ghaf-xdg-open/default.nix +++ b/packages/ghaf-xdg-open/default.nix @@ -6,6 +6,7 @@ dnsutils, openssh, sshKeyPath, + user, ... }: # This script is executed in the GUIVM by the Ghaf XDG systemd service when it receives an XDG open request. @@ -26,7 +27,6 @@ writeShellApplication { businessvmip=$(dig +short business-vm | head -1) commsvmip=$(dig +short comms-vm | head -1) - if [[ "127.0.0.1" != "$REMOTE_ADDR" && \ "$businessvmip" != "$REMOTE_ADDR" && \ "$googlechromevmip" != "$REMOTE_ADDR" && \ @@ -37,23 +37,23 @@ writeShellApplication { if [[ "127.0.0.1" != "$REMOTE_ADDR" ]]; then echo "Copying $sourcepath from $REMOTE_ADDR to $zathurapath in zathura-vm" - scp -i ${sshKeyPath} -o StrictHostKeyChecking=no "$REMOTE_ADDR":"$sourcepath" zathura-vm:"$zathurapath" + scp -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@"$REMOTE_ADDR":"$sourcepath" ${user}@zathura-vm:"$zathurapath" else echo "Copying $sourcepath from GUIVM to $zathurapath in zathura-vm" - scp -i ${sshKeyPath} -o StrictHostKeyChecking=no "$sourcepath" zathura-vm:"$zathurapath" + scp -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@"$sourcepath" ${user}@zathura-vm:"$zathurapath" fi echo "Opening $zathurapath in zathura-vm" if [[ "$type" == "pdf" ]]; then - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm run-waypipe zathura "'$zathurapath'" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm run-waypipe zathura "'$zathurapath'" elif [[ "$type" == "image" ]]; then - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm run-waypipe pqiv -i "'$zathurapath'" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm run-waypipe pqiv -i "'$zathurapath'" else echo "Unknown type: $type" fi echo "Deleting $zathurapath in zathura-vm" - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm rm -f "$zathurapath" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm rm -f "$zathurapath" ''; } diff --git a/packages/ssh-keys-helper/default.nix b/packages/ssh-keys-helper/default.nix index e0dabb719d..9ebe05c772 100644 --- a/packages/ssh-keys-helper/default.nix +++ b/packages/ssh-keys-helper/default.nix @@ -6,7 +6,7 @@ source = let script = pkgs.writeShellScriptBin config.ghaf.security.sshKeys.getAuthKeysFileName '' - [[ "$1" != "ghaf" ]] && exit 0 + [[ "$1" != "${config.ghaf.users.appUser.name}" && "$1" != "${config.ghaf.users.admin.name}" ]] && exit 0 ${pkgs.coreutils}/bin/cat ${config.ghaf.security.sshKeys.waypipeSshPublicKeyFile} ''; in diff --git a/packages/wifi-signal-strength/default.nix b/packages/wifi-signal-strength/default.nix index 427e875a3b..1fe1a6440e 100644 --- a/packages/wifi-signal-strength/default.nix +++ b/packages/wifi-signal-strength/default.nix @@ -31,17 +31,17 @@ writeShellApplication { flock -w 60 -x 99 || exit 1 # Return the result as json format for waybar and use the control socket to close the ssh tunnel. - trap 'ssh -q -S /tmp/nmcli_socket -O exit ghaf@net-vm && cat "$NETWORK_STATUS_FILE"' EXIT + trap 'ssh -q -S /tmp/nmcli_socket -O exit ${config.ghaf.users.admin.name}@net-vm && cat "$NETWORK_STATUS_FILE"' EXIT # Connect to netvm ssh -M -S /tmp/nmcli_socket \ - -f -N -q ghaf@net-vm \ + -f -N -q ${config.ghaf.users.admin.name}@net-vm \ -i /run/waypipe-ssh/id_ed25519 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o StreamLocalBindUnlink=yes \ -o ExitOnForwardFailure=yes \ - -L /tmp/ssh_session_dbus.sock:/run/user/${builtins.toString config.ghaf.users.accounts.uid}/bus \ + -L /tmp/ssh_session_dbus.sock:/run/user/${builtins.toString config.ghaf.users.admin.uid}/bus \ -L /tmp/ssh_system_dbus.sock:/run/dbus/system_bus_socket signal0="\UF091F" signal1="\UF0922"