Repository: ponkila/HomestakerOS
Pull request #82: modules, homestakeros: autogen ssv keys and then some
Author: login: tupakkatapa
Created: 2025-04-02T23:22:40Z | Merged: 2025-04-10T06:15:50Z

Proposal, not tested. Aiming to reduce the threshold for setting up a SSV. 

Keys would be generated automatically at boot if they do not exist, and `addons.ssv-node.dataDir` is a persistent location. The `dataDir` option defaults to the first mount option's "where" path + /ssv subdirectory; if none of the mounts exist, it falls back to /mnt/addons/ssv, however this fallback path would not result in automatic key generation since this location is probably in RAM.

Lastly, the node would only activate when the operator is properly registered. Registration is checked by querying the SSV Network API in 10 minute intervals.


Diff:
diff --git a/docs/homestakeros/3.1-wireguard_vpn.md b/docs/homestakeros/3.1-wireguard_vpn.md
index 173652c..8b3fbd8 100644
--- a/docs/homestakeros/3.1-wireguard_vpn.md
+++ b/docs/homestakeros/3.1-wireguard_vpn.md
@@ -24,15 +24,7 @@ These services simplify the process of generating keys, managing peers, and dist
 
 The following steps should be performed directly on your HomestakerOS node:
 
-1. **Install WireGuard tools**:
-
-   ```bash
-   nix-shell -p wireguard-tools
-   ```
-
-   This will start an interactive shell with the necessary command-line tools. Use this shell to run the subsequent commands.
-
-2. **Generate a key pair**:
+1. **Generate a key pair**:
 
    ```bash
    wg genkey | tee clientPrivateKey | wg pubkey > clientPublicKey
@@ -40,7 +32,7 @@ The following steps should be performed directly on your HomestakerOS node:
 
    This generates a cryptographically secure private key and its corresponding public key. WireGuard uses these for authenticated encryption - ensuring only authorized peers can connect to your network while protecting all traffic with strong encryption.
 
-3. **Create and edit the WireGuard configuration file**:
+2. **Create and edit the WireGuard configuration file**:
 
    ```bash
    sudo vim /mnt/secrets/wg0.conf
diff --git a/docs/homestakeros/3.2-ssv_node.md b/docs/homestakeros/3.2-ssv_node.md
index c5fac62..f367ff3 100644
--- a/docs/homestakeros/3.2-ssv_node.md
+++ b/docs/homestakeros/3.2-ssv_node.md
@@ -13,30 +13,41 @@ Running an SSV node allows you to participate in distributed validator technolog
 
 > **Tip:** 💡 Alternatively, for operator keys generation and registration, you can follow the [SSV Network Instructions](https://ssv-network.gitbook.io/guides/operator/registering-an-operator).
 
-## Generate SSV Operator Keys
+## Configure SSV Node in the Web UI
 
-> **Note:** The commands in this section require root privileges. Before proceeding, switch to the root user by running: `sudo su`
+SSV-node is integrated into HomestakerOS by default and does not require explicit enabling. The only requirement is ensuring the data directory is set to a persistent location where the SSV node will store its operational data and required operator keys, which are automatically generated if missing. The expects files: `ssv_operator_key`, `ssv_operator_key.pub`, and `password`.
 
-The following steps should be performed directly on your HomestakerOS node:
+> **Note:** If you already have a persistent mount at the SSV-node `dataDir` default location (`/mnt/addons/ssv`), the directory and keys are already created for you. Skip to the next section. However, if this default path is not suitable for your setup, please continue by reconfiguring the path and updating your node. You can remove any automatically generated files from the old location.
 
-1. **Create the SSV directory**:
+1. **Access the Web UI**:
+   - Start the backend and access the Web UI as described in [Accessing the Web UI](2.2-accessing_webui.md)
+   - Set the flake URI and load your configurations
 
-   ```bash
-   mkdir -p /mnt/addons/ssv
-   cd /mnt/addons/ssv
-   ```
+2. **Configure SSV-Node**:
+   - Go to the 'NixOS config' tab and select a node from the list
+   - In the Addons section, set the `dataDir` path to a **persistent location**
+     > **Important:** This must be a path under a defined mount, otherwise the keys will not be generated and the node will not start.
+
+3. **Save and build**:
+   - Save your configuration
+   - Click the `#BUIDL` button to build your updated configuration
+   - Download the boot media and deploy to your node
+
+The build updates the SSV node paths in your HomestakerOS image, and it will automatically start the node once you've completed the registration (checked at 10-minute intervals).
 
-   Creating a dedicated directory keeps your SSV node configuration files organized and separate from other components.
+## Fetch SSV Operator Public Key
 
-2. **Generate a key pair**:
+1. **Locate the SSV data directory**:
+
+   As mentioned in the last section, the required operator keys are generated automatically at boot if they do not exist. The keys will be generated under the data directory set by the `dataDir` option. SSH into your node and locate this folder.
+
+2. **Retrieve your public key**:
 
    ```bash
-   nix run github:ponkila/HomestakerOS#init-ssv --accept-flake-config -- <MY_OPERATOR_PASSWORD>
+   cat /path/to/ssv_operator_key.pub
    ```
 
-   This command will generate three files: a `password` file containing your password, an encrypted private key in `ssv_operator_key`, and a public key in `ssv_operator_key.pub`. You'll need the public key for registration.
-
-   The key pair is essential for the secure operation of your SSV node. The private key must be kept secure and will be used to sign messages, while the public key identifies your operator on the network.
+   This public key is essential for the operator registration process in the next section. Make sure to copy it exactly as shown.
 
 ## Register as an SSV Operator
 
@@ -56,9 +67,9 @@ You can do this directly through the HomestakerOS Web UI:
    - Choose whether the operator will be private or public
    - Click the 'Register' button
    - Confirm the transaction in MetaMask
-        > The transaction permanently associates your public key with your Ethereum address on the blockchain.
+     > The transaction permanently associates your public key with your Ethereum address on the blockchain.
 
-3. **Verify registration**:
+3. **Wait for confirmation**:
    - A transaction link will appear in the UI
    - Once the transaction is completed, the new operator will appear on SSV.Network
 
@@ -66,55 +77,35 @@ You can do this directly through the HomestakerOS Web UI:
 
 For your SSV node to function properly on the network, it needs to communicate with other nodes. For optimal network connectivity, consider port forwarding TCP port 13001 and UDP port 12001 on your router. However, this is not mandatory if your router supports NAT-PMP or UPnP protocols, which can automatically handle port mapping. Proper network connectivity ensures your SSV node can communicate effectively with the network and participate in its duties.
 
-## Re-Configure SSV Node in the Web UI
-
-> **Important**: If you stored the files exactly at the same paths as the defaults, which are:
-> - dataDir: `/mnt/addons/ssv`
-> - privateKeyFile: `/mnt/addons/ssv/ssv_operator_key`
-> - privateKeyPasswordFile: `/mnt/addons/ssv/password`
->
-> You do not need to re-configure nor rebuild. SSV-node is integrated into HomestakerOS by default and will start automatically when the keys are present (checked at 10-minute intervals). Skip to the "Verifying Your SSV Node" section.
-
-Now that your operator is registered, configure your SSV node:
-
-1. **Access the configuration tab**:
-   - Go to the 'NixOS config' tab
-   - Select your node from the list or create a new one
-
-2. **Configure SSV-Node**:
-   - In the Add-ons section, find SSV-Node
-   - Set `dataDir` to the directory where SSV node will store its operational data
-   - Set `privateKeyFile` to path to your encrypted private key
-   - Set `privateKeyPasswordFile` to path to your password file
+## Verifying Your SSV Node
 
-3. **Save and build**:
-   - Click the 'Save' button
-   - Click the `#BUIDL` button to build your updated configuration
-   - Download the boot media and deploy to your node
+After deployment, verify that your SSV node is running correctly:
 
-The build updates the SSV node paths in your HomestakerOS image, configuring it to start automatically on boot.
+1. **Verify operator registration**:
 
-## Verifying Your SSV Node
+   ```bash
+   curl -s https://api.ssv.network/api/v4/mainnet/operators/public_key/<your_public_key>
+   ```
 
-After deployment, verify that your SSV node is running correctly:
+   A registered operator will show complete details in the response.
 
-1. **Check the service status**:
+2. **Check the service status**:
 
    ```bash
-   systemctl status ssv-autostart.service
+   systemctl status ssv-node.service
    ```
 
    This confirms that the service is properly running without errors.
 
-2. **View the logs**:
+3. **View the logs**:
 
    ```bash
-   journalctl -fu ssv-autostart.service
+   journalctl -fu ssv-node.service
    ```
 
    This command displays real-time logs for the SSV service, helping you monitor its operation and troubleshoot any issues.
 
-3. **Check operator performance**:
+4. **Check operator performance**:
    - Return to the SSV Explorer and find your operator
    - Verify that it shows as active and is performing duties
 
diff --git a/docs/homestakeros/4-reference.md b/docs/homestakeros/4-reference.md
index d255244..dab9bd2 100644
--- a/docs/homestakeros/4-reference.md
+++ b/docs/homestakeros/4-reference.md
@@ -34,11 +34,10 @@ Execution layer client options (formerly known as Ethereum 1.0 clients).
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Erigon. |
-| `dataDir` | Path | `"/var/mnt/erigon"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/erigon"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:8551"` | HTTP-RPC server listening interface of engine API. |
-| `jwtSecretFile` | String | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/erigon/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | String | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ### Geth
@@ -46,11 +45,10 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Geth. |
-| `dataDir` | Path | `"/var/mnt/geth"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/geth"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:8551"` | HTTP-RPC server listening interface of engine API. |
-| `jwtSecretFile` | String | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/geth/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | String | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ### Nethermind
@@ -58,11 +56,10 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Nethermind. |
-| `dataDir` | Path | `"/var/mnt/nethermind"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/nethermind"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:8551"` | HTTP-RPC server listening interface of engine API. |
-| `jwtSecretFile` | String | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/nethermind/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | String | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ### Besu
@@ -70,11 +67,10 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Besu. |
-| `dataDir` | Path | `"/var/mnt/besu"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/besu"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:8551"` | HTTP-RPC server listening interface of engine API. |
-| `jwtSecretFile` | String | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/besu/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | String | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ## Consensus Clients
@@ -88,12 +84,11 @@ Consensus layer client options (formerly known as Ethereum 2.0 clients).
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Lighthouse. |
-| `dataDir` | Path | `"/var/mnt/lighthouse"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/lighthouse"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:5052"` | HTTP server listening interface. |
 | `execEndpoint` | String | `"http://127.0.0.1:8551"` | Server endpoint for an execution layer JWT-authenticated HTTP JSON-RPC connection. |
-| `jwtSecretFile` | Path | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/lighthouse/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | Path | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 | `slasher.enable` | Boolean | `false` | Whether to enable slasher. |
 | `slasher.historyLength` | Integer | `4096` | Number of epochs to store. |
@@ -104,12 +99,11 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Prysm. |
-| `dataDir` | Path | `"/var/mnt/prysm"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/prysm"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:3500"` | JSON-HTTP server listening interface. |
 | `execEndpoint` | String | `"http://127.0.0.1:8551"` | Server endpoint for an execution layer JWT-authenticated HTTP JSON-RPC connection. |
-| `jwtSecretFile` | Path | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/prysm/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | Path | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 | `slasher.enable` | Boolean | `false` | Whether to enable historical slasher. |
 
@@ -118,12 +112,11 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Nimbus. |
-| `dataDir` | Path | `"/var/mnt/nimbus"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/nimbus"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:5052"` | JSON-HTTP server listening interface. |
 | `execEndpoint` | String | `"http://127.0.0.1:8551"` | Server endpoint for an execution layer JWT-authenticated HTTP JSON-RPC connection. |
-| `jwtSecretFile` | Path | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/nimbus/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | Path | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ### Teku
@@ -131,12 +124,11 @@ Example: `["--some-extra-option=value"]` |
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable Teku. |
-| `dataDir` | Path | `"/var/mnt/teku"` | Data directory for the blockchain. |
+| `dataDir` | Path | `"/mnt/teku"` | Data directory for the blockchain. |
 | `endpoint` | String | `"http://127.0.0.1:5051"` | JSON-HTTP server listening interface. |
 | `execEndpoint` | String | `"http://127.0.0.1:8551"` | Server endpoint for an execution layer JWT-authenticated HTTP JSON-RPC connection. |
-| `jwtSecretFile` | Path | `null` | Path to the token that ensures safe connection between CL and EL.
-Example: `"/var/mnt/teku/jwt.hex"` |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `jwtSecretFile` | Path | `"/mnt/secrets/jwt.hex"` | Path to the token that ensures safe connection between CL and EL. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ## Add-ons
@@ -151,7 +143,7 @@ Additional services and tools to enhance your staking setup.
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable MEV-Boost. |
 | `endpoint` | String | `"http://127.0.0.1:18550"` | Listening interface for the MEV-Boost server. |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ### SSV-Node
@@ -160,10 +152,10 @@ Example: `["--some-extra-option=value"]` |
 
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
-| `dataDir` | Path | `"/var/mnt/addons/ssv"` | Path to a persistent directory to store the node's database. |
-| `privateKeyFile` | Path | `"/var/mnt/addons/ssv/ssv_operator_key"` | Path to the private SSV operator key. |
-| `privateKeyPasswordFile` | Path | `"/var/mnt/addons/ssv/password"` | Path to the password file of SSV operator key. |
-| `extraOptions` | List of Strings | `null` | Additional command-line arguments.
+| `dataDir` | Path | `"/mnt/addons/ssv"` | Path to a persistent directory to store the node's database and keys.
+Expected files: ssv_operator_key, ssv_operator_key.pub, and password.
+Keys will be generated automatically if missing. |
+| `extraOptions` | List of Strings | `[]` | Additional command-line arguments.
 Example: `["--some-extra-option=value"]` |
 
 ## Localization
@@ -174,7 +166,7 @@ Settings related to system localization.
 |--------|------|---------|-------------|
 | `hostname` | String | `"homestaker"` | The name of the machine.
 Must contain only alphanumeric characters, hyphens, or underscores, with a length between 1-63 characters. |
-| `timezone` | String | `null` | The time zone used when displaying times and dates.
+| `timezone` | String | `"UTC"` | The time zone used when displaying times and dates.
 Example: `"America/New_York"` |
 
 ## Mounts
@@ -195,9 +187,9 @@ Each mount entry has the following options:
 | `type` | String | `"auto"` | File system type.
 Example: `"btrfs"` |
 | `wantedBy` | List of Strings | `["multi-user.target"]` | Units that want (i.e. depend on) this unit. |
-| `what` | String | `null` (Required) | Absolute path of device node, file or other resource.
+| `what` | String | (Required) | Absolute path of device node, file or other resource.
 Example: `"/dev/disk/by-label/homestaker"` |
-| `where` | String | `null` (Required) | Absolute path of a directory of the mount point.
+| `where` | String | (Required) | Absolute path of a directory of the mount point.
 Will be created if it doesn't exist.
 Example: `"/mnt/erigon"` |
 
@@ -208,9 +200,8 @@ SSH server configuration for secure remote access to your node.
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `authorizedKeys` | List of Strings | `[]` | A list of public SSH keys to be added to the user's authorized keys. |
-| `privateKeyFile` | Path | `null` | Path to the Ed25519 SSH host key.
-If absent, the key will be generated automatically.
-Example: `"/var/mnt/secrets/ssh/id_ed25519"` |
+| `privateKeyFile` | Path | `"/mnt/secrets/ssh/id_ed25519"` | Path to the Ed25519 SSH host key.
+If absent, the key will be generated automatically. |
 
 ## VPN
 
@@ -221,4 +212,4 @@ Virtual Private Network configurations for secure communications.
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `enable` | Boolean | `false` | Whether to enable WireGuard. |
-| `configFile` | Path | `"/var/mnt/secrets/wg0.conf"` | A file path for the wg-quick configuration. |
+| `configFile` | Path | `"/mnt/secrets/wg0.conf"` | A file path for the wg-quick configuration. |
diff --git a/flake.lock b/flake.lock
index ab50d59..d77c8b9 100644
--- a/flake.lock
+++ b/flake.lock
@@ -750,11 +750,11 @@
     },
     "nixpkgs_6": {
       "locked": {
-        "lastModified": 1738574474,
-        "narHash": "sha256-rvyfF49e/k6vkrRTV4ILrWd92W+nmBDfRYZgctOyolQ=",
+        "lastModified": 1734875076,
+        "narHash": "sha256-Pzyb+YNG5u3zP79zoi8HXYMs15Q5dfjDgwCdUI5B0nY=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "fecfeb86328381268e29e998ddd3ebc70bbd7f7c",
+        "rev": "1807c2b91223227ad5599d7067a61665c52d1295",
         "type": "github"
       },
       "original": {
@@ -797,11 +797,11 @@
       },
       "locked": {
         "dir": "nixosModules/base",
-        "lastModified": 1735052886,
-        "narHash": "sha256-1KUjXJ3lneHZyFZL+GgBZVfZ4kkhUMZ6gohC4LXURnM=",
+        "lastModified": 1743861196,
+        "narHash": "sha256-DO9LxuY2IttEdJR57Ih/D5P2GUTRZtLuZ6jdbvHg1Uk=",
         "owner": "ponkila",
         "repo": "HomestakerOS",
-        "rev": "a5213786b62b5ac068801d9390ccc8380728b50c",
+        "rev": "09194b91b7dd144ecf63bfea346aee4cb5c16e6c",
         "type": "github"
       },
       "original": {
diff --git a/nixosModules/base/flake.nix b/nixosModules/base/flake.nix
index e47e983..982b625 100644
--- a/nixosModules/base/flake.nix
+++ b/nixosModules/base/flake.nix
@@ -221,8 +221,11 @@
       environment.systemPackages = with pkgs; [
         btrfs-progs
         kexec-tools
+        openssl
         rsync
         vim
+        wireguard-tools
+        tree
       ];
 
       services.timesyncd.enable = true;
diff --git a/nixosModules/homestakeros/default.nix b/nixosModules/homestakeros/default.nix
index 4d41329..f0cdb33 100644
--- a/nixosModules/homestakeros/default.nix
+++ b/nixosModules/homestakeros/default.nix
@@ -7,9 +7,22 @@ let
   cfg = config.homestakeros;
 in
 {
-  inherit (import ./options.nix { inherit lib pkgs; }) options;
+  inherit (import ./options.nix { inherit lib pkgs cfg; }) options;
 
   config = with lib; let
+    # Function to check if a path is on a persistent mount
+    isPersistentPath = path:
+      let
+        mountsOnPath = lib.filterAttrs
+          (_name: mount:
+            mount.enable &&
+            lib.hasPrefix mount.where path &&
+            !(lib.elem mount.type [ "tmpfs" "overlay" "squashfs" ])
+          )
+          cfg.mounts;
+      in
+      mountsOnPath != { };
+
     # Function to parse a URL into its components
     parseEndpoint = endpoint:
       let
@@ -112,7 +125,7 @@ in
       mkIf true {
         services.openssh = {
           enable = true;
-          hostKeys = lib.mkIf (cfg.ssh.privateKeyFile != null) [
+          hostKeys = [
             {
               path = cfg.ssh.privateKeyFile;
               type = "ed25519";
@@ -193,17 +206,18 @@ in
     (
       mkIf
         (
-          cfg.addons.ssv-node.privateKeyFile
-          != null
-          && cfg.addons.ssv-node.privateKeyPasswordFile
-          != null
-          && pkgs.system == "x86_64-linux"
+          pkgs.system == "x86_64-linux"
           && length activeConsensusClients > 0
           && length activeExecutionClients > 0
+          && isPersistentPath cfg.addons.ssv-node.dataDir
         )
         {
-          systemd.services.ssv-autostart =
+          systemd.services.ssv-node =
             let
+              privateKeyFile = "${cfg.addons.ssv-node.dataDir}/ssv_operator_key";
+              publicKeyFile = "${cfg.addons.ssv-node.dataDir}/ssv_operator_key.pub";
+              privateKeyPasswordFile = "${cfg.addons.ssv-node.dataDir}/password";
+
               # TODO: This is a bad way to do this, prevents multiple instances
               executionClient = builtins.elemAt activeExecutionClients 0;
               consensusClient = builtins.elemAt activeConsensusClients 0;
@@ -232,31 +246,51 @@ in
                   ETH1Addr: ws://${parsedExecutionEndpoint.addr}:8546
 
                 KeyStore:
-                  PrivateKeyFile: ${cfg.addons.ssv-node.privateKeyFile}
-                  PasswordFile: ${cfg.addons.ssv-node.privateKeyPasswordFile}
+                  PrivateKeyFile: ${privateKeyFile}
+                  PasswordFile: ${privateKeyPasswordFile}
               '';
             in
             {
-              description = "Start the SSV node if the private operator key exists";
-              unitConfig.ConditionPathExists = [
-                "${cfg.addons.ssv-node.privateKeyFile}"
-                "${cfg.addons.ssv-node.privateKeyPasswordFile}"
-              ];
-              # The operator key is defined here, so it does not need to be evaluated
-              script = ''
-                ${pkgs.ssvnode}/bin/ssvnode start-node --config ${ssvConfig}
-              '';
+              description = "Operator node for Secret Shared Validators (SSV)";
+              after = [ "network-online.target" ];
+              wants = [ "network-online.target" ];
               wantedBy = [ "multi-user.target" ];
+
+              script =
+                let
+                  curl = "${pkgs.curl}/bin/curl";
+                  jq = "${pkgs.jq}/bin/jq";
+                in
+                ''
+                  mkdir -p ${cfg.addons.ssv-node.dataDir}
+
+                  # Check if keys exist
+                  if [ ! -f "${privateKeyFile}" ] || [ ! -f "${publicKeyFile}" ]; then
+                    # Generate keys with timestamp as a password
+                    ${pkgs.init-ssv}/bin/init-ssv \
+                      --private-key "${privateKeyFile}" \
+                      --public-key "${publicKeyFile}" \
+                      --password-file "${privateKeyPasswordFile}" \
+                      $(date +%s) || exit 1
+                  fi
+
+                  # Start the node if operator is registered
+                  SSV_PUBLIC_KEY=$(cat "${publicKeyFile}")
+                  if ${curl} -s "https://api.ssv.network/api/v4/mainnet/operators/public_key/$SSV_PUBLIC_KEY" | ${jq} -e '.data != null' > /dev/null; then
+                    echo "operator is registered, starting ssv node..."
+                    ${pkgs.ssvnode}/bin/ssvnode start-node --config ${ssvConfig}
+                  else
+                    echo "error: operator is not registered yet, exiting"
+                    exit 1
+                  fi
+                '';
               serviceConfig = {
-                Restart = "always";
-                RestartSec = "5s";
                 Type = "simple";
+                Restart = "on-failure";
+                RestartSec = "600s"; # 10 minutes
               };
             };
-          systemd.timers.ssv-autostart = {
-            timerConfig.OnBootSec = "10min";
-            wantedBy = [ "timers.target" ];
-          };
+
           # Firewall
           networking.firewall = {
             allowedTCPPorts = [ 13001 ];
@@ -291,11 +325,7 @@ in
           "--authrpc.vhosts \"*\""
           "--authrpc.port ${parsedEndpoint.port}"
           "--authrpc.addr ${parsedEndpoint.addr}"
-          (
-            if cfg.execution.erigon.jwtSecretFile != null
-            then "--authrpc.jwtsecret ${cfg.execution.erigon.jwtSecretFile}"
-            else ""
-          )
+          "--authrpc.jwtsecret ${cfg.execution.erigon.jwtSecretFile}"
           # json-rpc for interacting
           "--http.addr=${parsedEndpoint.addr}"
           "--http.api=eth,erigon,web3,net,debug,trace,txpool"
@@ -306,7 +336,7 @@ in
           # ws for ssv
           "--ws"
         ]
-        ++ (if cfg.execution.erigon.extraOptions != null then cfg.execution.erigon.extraOptions else [ ]);
+        ++ cfg.execution.erigon.extraOptions;
         allowedPorts = [ 30303 30304 42069 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -330,11 +360,7 @@ in
           "--authrpc.vhosts \"*\""
           "--authrpc.port ${parsedEndpoint.port}"
           "--authrpc.addr ${parsedEndpoint.addr}"
-          (
-            if cfg.execution.geth.jwtSecretFile != null
-            then "--authrpc.jwtsecret ${cfg.execution.geth.jwtSecretFile}"
-            else ""
-          )
+          "--authrpc.jwtsecret ${cfg.execution.geth.jwtSecretFile}"
           # json-rpc for interacting
           "--http.addr=${parsedEndpoint.addr}"
           "--http.api=eth,web3,net,debug,txpool"
@@ -348,7 +374,7 @@ in
           "--ws.port=8545"
           "--ws"
         ]
-        ++ (if cfg.execution.geth.extraOptions != null then cfg.execution.geth.extraOptions else [ ]);
+        ++ cfg.execution.geth.extraOptions;
         allowedPorts = [ 30303 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -371,11 +397,7 @@ in
           # auth for consensus client
           "--JsonRpc.EngineHost ${parsedEndpoint.addr}"
           "--JsonRpc.EnginePort ${parsedEndpoint.port}"
-          (
-            if cfg.execution.nethermind.jwtSecretFile != null
-            then "--JsonRpc.JwtSecretFile ${cfg.execution.nethermind.jwtSecretFile}"
-            else ""
-          )
+          "--JsonRpc.JwtSecretFile ${cfg.execution.nethermind.jwtSecretFile}"
           # json-rpc for interacting
           "--JsonRpc.Enabled true"
           "--JsonRpc.Host ${parsedEndpoint.addr}"
@@ -384,7 +406,7 @@ in
           "--Init.WebSocketsEnabled true"
           "--JsonRpc.WebSocketsPort 8545"
         ]
-        ++ (if cfg.execution.nethermind.extraOptions != null then cfg.execution.nethermind.extraOptions else [ ]);
+        ++ cfg.execution.nethermind.extraOptions;
         allowedPorts = [ 30303 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -409,11 +431,7 @@ in
           "--engine-host-allowlist=\"*\""
           "--engine-rpc-port=${parsedEndpoint.port}"
           "--rpc-http-host=${parsedEndpoint.addr}"
-          (
-            if cfg.execution.besu.jwtSecretFile != null
-            then "--engine-jwt-secret=${cfg.execution.besu.jwtSecretFile}"
-            else ""
-          )
+          "--engine-jwt-secret=${cfg.execution.besu.jwtSecretFile}"
           # json-rpc for interacting
           "--rpc-http-api=ETH,NET,WEB3,TRACE,TXPOOL,DEBUG"
           "--rpc-http-authentication-enabled=false"
@@ -427,7 +445,7 @@ in
           "--rpc-ws-host=${parsedEndpoint.addr}"
           "--rpc-ws-port=8546"
         ]
-        ++ (if cfg.execution.besu.extraOptions != null then cfg.execution.besu.extraOptions else [ ]);
+        ++ cfg.execution.besu.extraOptions;
         allowedPorts = [ 30303 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -456,7 +474,7 @@ in
             ]}"
           "-addr ${parsedEndpoint.addr}:${parsedEndpoint.port}"
         ]
-        ++ (if cfg.addons.mev-boost.extraOptions != null then cfg.addons.mev-boost.extraOptions else [ ]);
+        ++ cfg.addons.mev-boost.extraOptions;
         allowedPorts = [ ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -480,11 +498,7 @@ in
           "--http-port ${parsedEndpoint.port}"
           "--http-allow-origin \"*\""
           "--execution-endpoint ${cfg.consensus.lighthouse.execEndpoint}"
-          (
-            if cfg.consensus.lighthouse.jwtSecretFile != null
-            then "--execution-jwt ${cfg.consensus.lighthouse.jwtSecretFile}"
-            else ""
-          )
+          "--execution-jwt ${cfg.consensus.lighthouse.jwtSecretFile}"
           "--prune-payloads false"
           (
             if cfg.consensus.lighthouse.slasher.enable
@@ -505,7 +519,7 @@ in
           "--metrics"
           "--checkpoint-sync-url \"https://beaconstate.info\""
         ]
-        ++ (if cfg.consensus.lighthouse.extraOptions != null then cfg.consensus.lighthouse.extraOptions else [ ]);
+        ++ cfg.consensus.lighthouse.extraOptions;
         allowedPorts = [ 9000 9001 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -527,11 +541,7 @@ in
           "--grpc-gateway-host ${parsedEndpoint.addr}"
           "--grpc-gateway-port ${parsedEndpoint.port}"
           "--execution-endpoint ${cfg.consensus.prysm.execEndpoint}"
-          (
-            if cfg.consensus.prysm.jwtSecretFile != null
-            then "--jwt-secret ${cfg.consensus.prysm.jwtSecretFile}"
-            else ""
-          )
+          "--jwt-secret ${cfg.consensus.prysm.jwtSecretFile}"
           (
             if cfg.addons.mev-boost.enable
             then "--http-mev-relay ${cfg.addons.mev-boost.endpoint}"
@@ -548,7 +558,7 @@ in
           "--checkpoint-sync-url=https://beaconstate.info"
           "--genesis-beacon-api-url=https://beaconstate.info"
         ]
-        ++ (if cfg.consensus.prysm.extraOptions != null then cfg.consensus.prysm.extraOptions else [ ]);
+        ++ cfg.consensus.prysm.extraOptions;
         allowedPorts = [ 9000 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -571,11 +581,7 @@ in
           "--rest-api-interface=${parsedEndpoint.addr}"
           "--rest-api-host-allowlist=\"*\""
           "--ee-endpoint=${cfg.consensus.teku.execEndpoint}"
-          (
-            if cfg.consensus.teku.jwtSecretFile != null
-            then "--ee-jwt-secret-file=${cfg.consensus.teku.jwtSecretFile}"
-            else ""
-          )
+          "--ee-jwt-secret-file=${cfg.consensus.teku.jwtSecretFile}"
           (
             if cfg.addons.mev-boost.enable
             then "--builder-endpoint=${cfg.addons.mev-boost.endpoint}"
@@ -583,7 +589,7 @@ in
           )
           "--metrics-enabled=true"
         ]
-        ++ (if cfg.consensus.teku.extraOptions != null then cfg.consensus.teku.extraOptions else [ ]);
+        ++ cfg.consensus.teku.extraOptions;
         allowedPorts = [ 9000 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
@@ -616,7 +622,7 @@ in
           )
           "--metrics=true"
         ]
-        ++ (if cfg.consensus.nimbus.extraOptions != null then cfg.consensus.nimbus.extraOptions else [ ]);
+        ++ cfg.consensus.nimbus.extraOptions;
         allowedPorts = [ 9000 ];
       in
       createService serviceName serviceType execStart parsedEndpoint allowedPorts
diff --git a/nixosModules/homestakeros/options.nix b/nixosModules/homestakeros/options.nix
index 7746488..3c51354 100644
--- a/nixosModules/homestakeros/options.nix
+++ b/nixosModules/homestakeros/options.nix
@@ -1,6 +1,7 @@
 { lib
 , ...
-}: {
+}:
+{
   options.homestakeros = with lib; {
     localization = {
       hostname = mkOption {
@@ -9,8 +10,8 @@
         description = "The name of the machine.";
       };
       timezone = mkOption {
-        type = types.nullOr types.str;
-        default = null;
+        type = types.str;
+        default = "UTC";
         description = "The time zone used when displaying times and dates.";
         example = "America/New_York";
       };
@@ -38,7 +39,7 @@
           where = mkOption {
             type = types.str;
             description = ''
-              Absolute path of a directory of the mount point. Will be created if it doesn’t exist. (Mandatory)
+              Absolute path of a directory of the mount point. Will be created if it doesn't exist. (Mandatory)
             '';
             example = "/mnt";
           };
@@ -109,10 +110,9 @@
         description = "A list of public SSH keys to be added to the user's authorized keys.";
       };
       privateKeyFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
+        type = types.path;
+        default = "/mnt/secrets/ssh/id_ed25519";
         description = "Path to the Ed25519 SSH host key. If absent, the key will be generated automatically.";
-        example = "/mnt/secrets/ssh/id_ed25519";
       };
     };
 
@@ -134,14 +134,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
+          type = types.str;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/erigon/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -164,14 +163,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
+          type = types.str;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/geth/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -194,14 +192,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
+          type = types.str;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/nethermind/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -224,14 +221,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
+          type = types.str;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/besu/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -278,14 +274,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
+          type = types.path;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/lighthouse/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -320,14 +315,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
+          type = types.path;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/prysm/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -355,14 +349,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
+          type = types.path;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/teku/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -390,14 +383,13 @@
           description = "Data directory for the blockchain.";
         };
         jwtSecretFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
+          type = types.path;
+          default = "/mnt/secrets/jwt.hex";
           description = "Path to the token that ensures safe connection between CL and EL.";
-          example = "/mnt/nimbus/jwt.hex";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -406,24 +398,18 @@
 
     addons = {
       ssv-node = {
-        privateKeyFile = mkOption {
-          type = types.nullOr types.path;
-          default = "/mnt/addons/ssv/ssv_operator_key";
-          description = "Path to the private SSV operator key.";
-        };
-        privateKeyPasswordFile = mkOption {
-          type = types.nullOr types.path;
-          default = "/mnt/addons/ssv/password";
-          description = "Path to the password file of SSV operator key";
-        };
         dataDir = mkOption {
           type = types.path;
           default = "/mnt/addons/ssv";
-          description = "Path to a persistent directory to store the node's database.";
+          description = ''
+            Path to a persistent directory to store the node's database and keys.
+            Expected files: ssv_operator_key, ssv_operator_key.pub, and password.
+            Keys will be generated automatically if missing.
+          '';
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
@@ -440,8 +426,8 @@
           description = "Listening interface for the MEV-Boost server.";
         };
         extraOptions = mkOption {
-          type = types.nullOr (types.listOf types.str);
-          default = null;
+          type = types.listOf types.str;
+          default = [ ];
           description = "Additional command-line arguments.";
           example = [ "--some-extra-option=value" ];
         };
diff --git a/packages/init-ssv/init-ssv.sh b/packages/init-ssv/init-ssv.sh
index 0647136..48c773f 100644
--- a/packages/init-ssv/init-ssv.sh
+++ b/packages/init-ssv/init-ssv.sh
@@ -1,56 +1,123 @@
 #!/usr/bin/env bash
 # Script to generate SSV operator keys with a password
 
-# Define file paths
-PRIVATE_KEY_PATH="ssv_operator_key"
-PUBLIC_KEY_PATH="ssv_operator_key.pub"
-PASSWORD_PATH="password"
+# Define default file paths
+PRIVATE_KEY_PATH="${PRIVATE_KEY_PATH:-ssv_operator_key}"
+PUBLIC_KEY_PATH="${PUBLIC_KEY_PATH:-ssv_operator_key.pub}"
+PASSWORD_PATH="${PASSWORD_PATH:-password}"
 TEMP_ENCRYPTED_PATH="encrypted_private_key.json"
 
-# Check if password is provided
-if [ "$#" -ne 1 ]; then
-    echo "Usage: init-ssv <password>"
-    exit 1
-fi
+# Display usage information
+display_usage() {
+  cat <<USAGE
+Usage: init-ssv [OPTIONS...] <PASSWORD>
 
-# Save password to file
-echo "$1" > "$PASSWORD_PATH"
-echo "saved password to '$PASSWORD_PATH'"
+Description:
+  Generate SSV operator keys with password encryption.
 
-# Generate keys and capture both stdout and stderr
-echo "generating SSV operator keys..."
-if ! keys_output=$(ssvnode generate-operator-keys 2>&1); then
-    echo "error: failed to generate keys"
-    echo "$keys_output"
-    exit 1
-fi
+Arguments:
+  PASSWORD
+    Password to encrypt the private key (required).
+
+Options:
+  -p, --private-key FILE     Set private key output file (default: $PRIVATE_KEY_PATH)
+  -b, --public-key FILE      Set public key output file (default: $PUBLIC_KEY_PATH)
+  -w, --password-path FILE   Set password file path (default: $PASSWORD_PATH)
+  -h, --help                 Show this help message
+USAGE
+  exit 0
+}
+
+# Main function
+main() {
+  # Parse command line arguments
+  while [[ $# -gt 0 ]]; do
+    case "$1" in
+      -p|--private-key)
+        PRIVATE_KEY_PATH="$2"
+        shift 2
+        ;;
+      -b|--public-key)
+        PUBLIC_KEY_PATH="$2"
+        shift 2
+        ;;
+      -w|--password-file)
+        PASSWORD_PATH="$2"
+        shift 2
+        ;;
+      -h|--help)
+        display_usage
+        ;;
+      -*)
+        echo "error: unknown option: $1"
+        display_usage
+        ;;
+      *)
+        # This should be the password
+        if [[ -z "$PASSWORD" ]]; then
+          PASSWORD="$1"
+          shift
+        else
+          echo "error: only one argument is allowed"
+          display_usage
+        fi
+        ;;
+    esac
+  done
+
+  # Validate required arguments
+  if [[ -z "$PASSWORD" ]]; then
+    echo "error: password must be provided"
+    display_usage
+  fi
 
-# Extract the public and private keys
-public_key=$(echo "$keys_output" | grep -o '{"pk":.*}' | jq -r '.pk')
-private_key=$(echo "$keys_output" | grep -o '{"sk":.*}' | jq -r '.sk')
+  # Ensure output directories exist
+  mkdir -p "$(dirname "$PRIVATE_KEY_PATH")"
+  mkdir -p "$(dirname "$PUBLIC_KEY_PATH")"
+  mkdir -p "$(dirname "$PASSWORD_PATH")"
 
-if [ -z "$public_key" ] || [ -z "$private_key" ]; then
+  # Save password to file
+  echo "$PASSWORD" > "$PASSWORD_PATH"
+  echo "saved password to '$PASSWORD_PATH'"
+
+  # Generate keys and capture both stdout and stderr
+  echo "generating ssv operator keys..."
+  if ! keys_output=$(ssvnode generate-operator-keys 2>&1); then
+      echo "error: failed to generate keys"
+      echo "$keys_output"
+      exit 1
+  fi
+
+  # Extract the public and private keys
+  public_key=$(echo "$keys_output" | grep -o '{"pk":.*}' | jq -r '.pk')
+  private_key=$(echo "$keys_output" | grep -o '{"sk":.*}' | jq -r '.sk')
+
+  if [[ -z "$public_key" ]] || [[ -z "$private_key" ]]; then
     echo "error: could not parse keys from output"
     echo "$keys_output"
     exit 1
-fi
+  fi
 
-# Save public key
-echo "$public_key" > "$PUBLIC_KEY_PATH"
-echo "saved public key to '$PUBLIC_KEY_PATH'"
+  # Save public key
+  echo "$public_key" > "$PUBLIC_KEY_PATH"
+  echo "saved public key to '$PUBLIC_KEY_PATH'"
 
-# Encrypt the private key and capture output
-echo "encrypting private key..."
-if ! encrypt_output=$(echo "$private_key" | ssvnode generate-operator-keys -p "$PASSWORD_PATH" -o /dev/stdin 2>&1) || [ ! -f "$TEMP_ENCRYPTED_PATH" ]; then
-    echo "error: failed to encrypt private key"
-    echo "$encrypt_output"
-    exit 1
-fi
+  # Encrypt the private key and capture output
+  echo "encrypting private key..."
+  if ! encrypt_output=$(echo "$private_key" | ssvnode generate-operator-keys -p "$PASSWORD_PATH" -o /dev/stdin 2>&1) || [ ! -f "$TEMP_ENCRYPTED_PATH" ]; then
+      echo "error: failed to encrypt private key"
+      echo "$encrypt_output"
+      exit 1
+  fi
+
+  # Move the encrypted key to the final location
+  mv "$TEMP_ENCRYPTED_PATH" "$PRIVATE_KEY_PATH"
+  echo "saved encrypted private key to '$PRIVATE_KEY_PATH'"
 
-# Move the encrypted key to the final location
-mv "$TEMP_ENCRYPTED_PATH" "$PRIVATE_KEY_PATH"
-echo "saved encrypted private key to '$PRIVATE_KEY_PATH'"
+  # Display the public key
+  echo "generated public key:"
+  cat "$PUBLIC_KEY_PATH"
+}
 
-# Display the public key
-echo "generated public key:"
-cat "$PUBLIC_KEY_PATH"
+# Run the main function
+main "$@"


Reviews:
user: login: jhvst
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T13:45:46Z
user: login: jhvst
body: lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T15:29:46Z

Review comments:
diff_hunk: @@ -1,6 +1,19 @@
 { lib
+, cfg
 , ...
-}: {
+}:
+let
+  # Try to get the first mount's path, fallback to null
path: nixosModules/homestakeros/options.nix
user: login: jhvst
body: Why default to null when you match on it later on to make it a string anyway?
Null is an anti-pattern: your return "type" is basically `Either` ( `String` | *Undefined* ).
Null is very bad because it has so much undefined behavior when its being ran against library functions, especially in languages like Nix which do not have strong typing. 
created_at: 2025-04-05T13:33:37Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,19 @@
 { lib
+, cfg
 , ...
-}: {
+}:
+let
+  # Try to get the first mount's path, fallback to null
+  firstMountPath =
+    let
+      mountNames = lib.attrNames (lib.filterAttrs (name: mount: mount.enable) cfg.mounts);
+      firstMountName = lib.optional (mountNames != [ ]) (builtins.head mountNames);
path: nixosModules/homestakeros/options.nix
user: login: jhvst
body: Make sure `mountNames` always returns an array.
created_at: 2025-04-05T13:36:26Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,19 @@
 { lib
+, cfg
 , ...
-}: {
+}:
+let
+  # Try to get the first mount's path, fallback to null
+  firstMountPath =
+    let
+      mountNames = lib.attrNames (lib.filterAttrs (name: mount: mount.enable) cfg.mounts);
+      firstMountName = lib.optional (mountNames != [ ]) (builtins.head mountNames);
+    in
+    if firstMountName != [ ]
+    then "${cfg.mounts.${builtins.head firstMountName}.where}"
+    else null;
path: nixosModules/homestakeros/options.nix
user: login: jhvst
body: Because when `mountNames` is always an array, you can always call array functions like head on it.
created_at: 2025-04-05T13:38:03Z
author_association: MEMBER
diff_hunk: @@ -237,26 +233,54 @@ in
               '';
             in
             {
-              description = "Start the SSV node if the private operator key exists";
-              unitConfig.ConditionPathExists = [
-                "${cfg.addons.ssv-node.privateKeyFile}"
-                "${cfg.addons.ssv-node.privateKeyPasswordFile}"
-              ];
-              # The operator key is defined here, so it does not need to be evaluated
-              script = ''
-                ${pkgs.ssvnode}/bin/ssvnode start-node --config ${ssvConfig}
-              '';
+              description = "Operator node for Secret Shared Validators (SSV)";
+              after = [ "network-online.target" ];
+              wants = [ "network-online.target" ];
               wantedBy = [ "multi-user.target" ];
+
+              script =
+                let
+                  curl = "${pkgs.curl}/bin/curl";
+                  jq = "${pkgs.jq}/bin/jq";
+                in
+                ''
+                  mkdir -p ${cfg.addons.ssv-node.dataDir}
+
+                  # Check if keys exist
+                  if [ ! -f "${cfg.addons.ssv-node.privateKeyFile}" ] || [ ! -f "${cfg.addons.ssv-node.publicKeyFile}" ]; then
+
+                    # Check if dataDir is on tmpfs
+                    FS_TYPE=$(df --output=fstype "${cfg.addons.ssv-node.dataDir}" | tail -n1)
+                    if [[ "$FS_TYPE" =~ ^(tmpfs|overlay|squashfs)$ ]]; then
+                      echo "error: '${cfg.addons.ssv-node.dataDir}' is on a tmpfs; generated keys would be lost after reboot"
+                      exit 1
+                    fi
path: nixosModules/homestakeros/default.nix
user: login: jhvst
body: Notice that you could have just use the `cfg` to track down the `dataDir` to the corresponding `systemd-mount` option hence get static guarantees rather than do this runtime checking.
created_at: 2025-04-05T13:41:46Z
author_association: MEMBER
path: packages/init-ssv/init-ssv.sh
user: login: jhvst
body: lgtm
created_at: 2025-04-05T13:42:04Z
author_association: MEMBER
diff_hunk: @@ -13,30 +13,29 @@ Running an SSV node allows you to participate in distributed validator technolog
 
 > **Tip:** 💡 Alternatively, for operator keys generation and registration, you can follow the [SSV Network Instructions](https://ssv-network.gitbook.io/guides/operator/registering-an-operator).
 
-## Generate SSV Operator Keys
+## Fetch SSV Operator Public Key
 
-> **Note:** The commands in this section require root privileges. Before proceeding, switch to the root user by running: `sudo su`
+> **Note:** The SSV node is integrated into HomestakerOS by default and does not require explicit enabling. It automatically generates the operator keys if the dataDir is a persistent location and the node will activate once your operator is properly registered on the SSV network.
 
-The following steps should be performed directly on your HomestakerOS node:
+1. **Locate the SSV data directory**:
 
-1. **Create the SSV directory**:
+   HomestakerOS automatically determines where to store SSV data:
+   - If you have configured mounts, it uses the first available mount point: `<first-mount-point>/ssv`
+   - If no mounts are configured, it defaults to `/mnt/addons/ssv`
 
-   ```bash
-   mkdir -p /mnt/addons/ssv
-   cd /mnt/addons/ssv
-   ```
+     > **Tip:** If this default path is not suitable for your setup:
+     > - Access the Web UI and navigate to the 'NixOS config' tab
+     > - In the Add-ons section, find SSV-Node and re-configure the `dataDir` path
+     > - Rebuild and deploy the update to your node to generate the keys at your preferred location
+     > - Remove any automatically generated files at the old location
 
-   Creating a dedicated directory keeps your SSV node configuration files organized and separate from other components.
-
-2. **Generate a key pair**:
+2. **Retrieve your public key**:
 
    ```bash
-   nix run github:ponkila/HomestakerOS#init-ssv --accept-flake-config -- <MY_OPERATOR_PASSWORD>
+   cat /path/to/ssv_operator_key.pub
    ```
 
-   This command will generate three files: a `password` file containing your password, an encrypted private key in `ssv_operator_key`, and a public key in `ssv_operator_key.pub`. You'll need the public key for registration.
-
-   The key pair is essential for the secure operation of your SSV node. The private key must be kept secure and will be used to sign messages, while the public key identifies your operator on the network.
+   This public key is essential for the operator registration process in the next section. Make sure to copy it exactly as shown.
path: docs/homestakeros/3.2-ssv_node.md
user: login: jhvst
body: Retrieve it on which host? Could they instead run some Nix command?
created_at: 2025-04-05T13:44:53Z
author_association: MEMBER
diff_hunk: @@ -58,63 +57,43 @@ You can do this directly through the HomestakerOS Web UI:
    - Confirm the transaction in MetaMask
         > The transaction permanently associates your public key with your Ethereum address on the blockchain.
 
-3. **Verify registration**:
+3. **Wait for confirmation**:
    - A transaction link will appear in the UI
    - Once the transaction is completed, the new operator will appear on SSV.Network
 
 ## Network Configuration
 
 For your SSV node to function properly on the network, it needs to communicate with other nodes. For optimal network connectivity, consider port forwarding TCP port 13001 and UDP port 12001 on your router. However, this is not mandatory if your router supports NAT-PMP or UPnP protocols, which can automatically handle port mapping. Proper network connectivity ensures your SSV node can communicate effectively with the network and participate in its duties.
 
-## Re-Configure SSV Node in the Web UI
-
-> **Important**: If you stored the files exactly at the same paths as the defaults, which are:
-> - dataDir: `/mnt/addons/ssv`
-> - privateKeyFile: `/mnt/addons/ssv/ssv_operator_key`
-> - privateKeyPasswordFile: `/mnt/addons/ssv/password`
->
-> You do not need to re-configure nor rebuild. SSV-node is integrated into HomestakerOS by default and will start automatically when the keys are present (checked at 10-minute intervals). Skip to the "Verifying Your SSV Node" section.
-
-Now that your operator is registered, configure your SSV node:
-
-1. **Access the configuration tab**:
-   - Go to the 'NixOS config' tab
-   - Select your node from the list or create a new one
-
-2. **Configure SSV-Node**:
-   - In the Add-ons section, find SSV-Node
-   - Set `dataDir` to the directory where SSV node will store its operational data
-   - Set `privateKeyFile` to path to your encrypted private key
-   - Set `privateKeyPasswordFile` to path to your password file
+## Verifying Your SSV Node
 
-3. **Save and build**:
-   - Click the 'Save' button
-   - Click the `#BUIDL` button to build your updated configuration
-   - Download the boot media and deploy to your node
+After deployment, verify that your SSV node is running correctly:
 
-The build updates the SSV node paths in your HomestakerOS image, configuring it to start automatically on boot.
+1. **Verify operator registration**:
 
-## Verifying Your SSV Node
+   ```bash
+   curl -s https://api.ssv.network/api/v4/mainnet/operators/public_key/<your_public_key>
+   ```
 
-After deployment, verify that your SSV node is running correctly:
+   A registered operator will show complete details in the response
 
-1. **Check the service status**:
+2. **Check the service status**:
 
    ```bash
    systemctl status ssv-autostart.service
path: docs/homestakeros/3.2-ssv_node.md
user: login: jhvst
body: Notice you changed the name of the unit file later in the PR.
created_at: 2025-04-05T13:45:23Z
author_association: MEMBER

PR comments:
user: login: tupakkatapa
created_at: 2025-04-03T04:11:16Z
body: @jhvst thoughts?
author_association: MEMBER
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-03T04:14:56Z
body: ## Deploying homestakeros with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>4e1265b</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://065068a3.homestakeros.pages.dev'>https://065068a3.homestakeros.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://jesse-improve-init-ssv.homestakeros.pages.dev'>https://jesse-improve-init-ssv.homestakeros.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/2c4d6da4d84548aeb9aafdf3e111b1c8/pages/view/homestakeros/065068a3-f235-4d7a-804b-e4322a07958d)

author_association: NONE
user: login: tupakkatapa
created_at: 2025-04-07T05:40:49Z
body: Since we need to be able to evaluate schema, circular import of cfg is unintuitive and results in an error. I suggest simplifying the approach by just removing the key file options and dataDir's first mount default completely.

```bash
[I] kari@torgue ~/W/p/HomestakerOS (jesse/improve-init-ssv)> nix eval --json .#schema | jq -r '.'
error:
       … while evaluating attribute 'addons'

       … while evaluating attribute 'ssv-node'

       … while evaluating attribute 'dataDir'

       … while evaluating attribute 'default'
         at /nix/store/50jy2msjzq74j5dngf8kld5r2bvmdb0f-source/introspect.nix:89:11:
           88|           type = typeString;
           89|           default = v.default or null;
             |           ^
           90|           description = v.description or null;

       … while evaluating the module argument `cfg' in "/nix/store/50jy2msjzq74j5dngf8kld5r2bvmdb0f-source/nixosModules/homestakeros/options.nix":

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: attribute 'cfg' missing
       at /nix/store/096c6m1vzz4zr46lhqc8c1ynwr4zpc0a-source/lib/modules.nix:515:28:
          514|         addErrorContext (context name)
          515|           (args.${name} or config._module.args.${name})
             |                            ^
          516|       ) (functionArgs f);
```

author_association: MEMBER
user: login: tupakkatapa
created_at: 2025-04-07T05:46:46Z
body: Or then just scrap this for now and do this properly when agenix-rekey gets implemented.
author_association: MEMBER
user: login: jhvst
created_at: 2025-04-07T10:11:54Z
body: > Since we need to be able to evaluate schema, circular import of cfg is unintuitive and results in an error. I suggest simplifying the approach by just removing the key file options and dataDir's first mount default completely.
> 
> ```shell
> [I] kari@torgue ~/W/p/HomestakerOS (jesse/improve-init-ssv)> nix eval --json .#schema | jq -r '.'
> error:
>        … while evaluating attribute 'addons'
> 
>        … while evaluating attribute 'ssv-node'
> 
>        … while evaluating attribute 'dataDir'
> 
>        … while evaluating attribute 'default'
>          at /nix/store/50jy2msjzq74j5dngf8kld5r2bvmdb0f-source/introspect.nix:89:11:
>            88|           type = typeString;
>            89|           default = v.default or null;
>              |           ^
>            90|           description = v.description or null;
> 
>        … while evaluating the module argument `cfg' in "/nix/store/50jy2msjzq74j5dngf8kld5r2bvmdb0f-source/nixosModules/homestakeros/options.nix":
> 
>        (stack trace truncated; use '--show-trace' to show the full, detailed trace)
> 
>        error: attribute 'cfg' missing
>        at /nix/store/096c6m1vzz4zr46lhqc8c1ynwr4zpc0a-source/lib/modules.nix:515:28:
>           514|         addErrorContext (context name)
>           515|           (args.${name} or config._module.args.${name})
>              |                            ^
>           516|       ) (functionArgs f);
> ```

Forgot about this, lets proceed with what we have.
author_association: MEMBER
user: login: tupakkatapa
created_at: 2025-04-07T14:36:56Z
body: I would still want to push the added improvements here:
- Automatic key generation; it removes one step that users have to do
- The registration check; much better than only checking if the keys exist
- Persistent check (now done at nix-level), giving lower chance of keys getting lost

Additionally, I've:
- Removed nulls, as you explained, and most of this configuration has to be defined anyways for a working setup
- Made examples (placeholders) into defaults, which creates a much nicer UI experience when the values are already there, especially when using the default `/mnt/{secrets,consensus,execution}` setup which the guide covers

Need to focus on testing for a couple of days now, making sure these changes work as they should. Give it a quick review and if everything works and makes sense to you as well, we might as well push this before the demo IMO.


author_association: MEMBER
user: login: tupakkatapa
created_at: 2025-04-07T15:38:04Z
body: Tested persistent and registeration check, fixed a syntax error. Set:
- `addons.ssv-node.dataDir` -> `/mnt/ethereum/addons/ssv`
- `mounts.<name>.where` -> `/mnt/ethereum`

```bash
core@homestakeros-test ~> ls /mnt/ethereum/addons/ssv
password  ssv_operator_key  ssv_operator_key.pub
core@homestakeros-test ~> journalctl -fu ssv-node.service
Apr 07 18:01:27 homestakeros-test systemd[1]: Started Operator node for Secret Shared Validators (SSV).
Apr 07 18:01:27 homestakeros-test ssv-node-start[974]: error: operator is not registered yet, exiting
Apr 07 18:01:27 homestakeros-test systemd[1]: ssv-node.service: Main process exited, code=exited, status=1/FAILURE
Apr 07 18:01:27 homestakeros-test systemd[1]: ssv-node.service: Failed with result 'exit-code'.
Apr 07 18:01:27 homestakeros-test systemd[1]: ssv-node.service: Consumed 29ms CPU time, 5.3M memory peak, 2.2M read from disk.
Apr 07 18:11:27 homestakeros-test systemd[1]: ssv-node.service: Scheduled restart job, restart counter is at 1.
Apr 07 18:11:27 homestakeros-test systemd[1]: Started Operator node for Secret Shared Validators (SSV).
Apr 07 18:11:28 homestakeros-test ssv-node-start[1479]: error: operator is not registered yet, exiting
Apr 07 18:11:28 homestakeros-test systemd[1]: ssv-node.service: Main process exited, code=exited, status=1/FAILURE
Apr 07 18:11:28 homestakeros-test systemd[1]: ssv-node.service: Failed with result 'exit-code'.
Apr 07 18:11:28 homestakeros-test systemd[1]: ssv-node.service: Consumed 84ms CPU time, 5.5M memory peak, 11K read from disk, 4.1K incoming IP traffic, 1.9K outgoing IP traffic.
```

Switched to registered key at this point..

```bash
Apr 07 18:21:28 homestakeros-test systemd[1]: ssv-node.service: Scheduled restart job, restart counter is at 2.
Apr 07 18:21:28 homestakeros-test systemd[1]: Started Operator node for Secret Shared Validators (SSV).
Apr 07 18:21:29 homestakeros-test ssv-node-start[2073]: operator is registered, starting ssv node...
Apr 07 18:21:29 homestakeros-test ssv-node-start[2079]: 2025-04-07T15:21:29.938032Z        INFO        starting SSV-Node:v2.0.0-e39f1d0ec67c47f6ba437d83367aba0d2b4d34dd
...
```

Set:
- `addons.ssv-node.dataDir` -> `/mnt/addons2/ssv`
- `mounts.<name>.where` -> `/mnt/ethereum`

```bash
core@homestakeros-test ~> ls /mnt/addons2/ssv
ls: cannot access '/mnt/addons2/ssv': No such file or directory
core@homestakeros-test ~ [2]> systemctl status ssv-node.service
Unit ssv-node.service could not be found.
```

author_association: MEMBER