diff --git a/README.md b/README.md index cb1e2c8..e32da0c 100755 --- a/README.md +++ b/README.md @@ -113,6 +113,122 @@ sudo nixos-rebuild switch --flake .#hostname home-manager switch --flake .#username@hostname ``` +## Secrets Management + +Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix). Each secret file is encrypted with [age](https://age-encryption.org/), using the SSH host key (`/etc/ssh/ssh_host_ed25519_key`) of each machine as a recipient, so that machine can decrypt its own secrets at boot without any passphrase. + +### How age keys work + +sops-nix derives an age key from the machine's ed25519 SSH host key automatically. The corresponding age **public key** must be added to `.sops.yaml` before you can encrypt secrets for that machine. + +To get the age public key for a machine: + +```bash +# On the target machine (or from its host key file): +nix-shell -p ssh-to-age --run \ + 'ssh-keyscan localhost 2>/dev/null | ssh-to-age' + +# Or directly from the key file: +nix-shell -p ssh-to-age --run \ + 'ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub' +``` + +### Adding a new machine + +1. **Get the age public key** for the new machine using the command above. + +2. **Add it to `.sops.yaml`**: + ```yaml + keys: + - &new-machine age1 + creation_rules: + - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - age: + - *new-machine + # ... existing recipients + ``` + +3. **Re-encrypt all secret files** so the new machine becomes a recipient: + ```bash + find secrets/ -name '*.yaml' -exec sops updatekeys {} \; + ``` + +### Adding a new secret + +To add a secret to an existing file: + +```bash +# Edit the file interactively (sops decrypts, opens $EDITOR, re-encrypts on save) +sops secrets/nas-secrets.yaml +``` + +To create a new secrets file: + +```bash +sops secrets/mymachine-secrets.yaml +``` + +The `.sops.yaml` `creation_rules` determine which keys encrypt the file based on its path. + +### Generating Nebula VPN certificates + +The Nebula module (`mjallen.services.nebula`) expects three secrets per host under a configurable prefix: +- `/ca-cert` — the CA certificate (shared across all nodes) +- `/host-cert` — this node's signed certificate +- `/host-key` — this node's private key + +**Step 1 — Create the CA** (once per network, on a trusted machine): + +```bash +nebula-cert ca -name "jallen-nebula" +# Produces: ca.crt, ca.key +``` + +**Step 2 — Sign a certificate for each node**: + +```bash +# Lighthouse (assign an overlay IP, e.g. 10.1.1.1) +nebula-cert sign -name "pi5" -ip "10.1.1.1/24" \ + -ca-crt ca.crt -ca-key ca.key \ + -out-crt lighthouse.crt -out-key lighthouse.key + +# Regular node (assign a unique overlay IP, e.g. 10.1.1.2) +nebula-cert sign -name "nas" -ip "10.1.1.2/24" \ + -ca-crt ca.crt -ca-key ca.key \ + -out-crt nas.crt -out-key nas.key +``` + +**Step 3 — Add the secrets to SOPS**: + +```bash +# Edit the target host's secrets file +sops secrets/pi5-secrets.yaml +``` + +Add the certificate contents under the configured prefix (e.g. `pi5/nebula`): + +```yaml +pi5: + nebula: + ca-cert: | + + lighthouse-cert: | + + lighthouse-key: | + +``` + +The key name for the cert/key pair matches the `hostSecretName` option (e.g. `hostSecretName = "lighthouse"` → looks for `lighthouse-cert` / `lighthouse-key`). + +**Step 4 — Shred the plaintext key files** once they are in SOPS: + +```bash +shred -u ca.key lighthouse.key nas.key +``` + +> Keep `ca.crt` accessible if you need to sign more nodes later, but store `ca.key` only in SOPS. + ## Documentation Comprehensive documentation is available in the [docs](./docs) directory: diff --git a/packages/cockpit-benchmark/default.nix b/packages/cockpit-benchmark/default.nix new file mode 100644 index 0000000..cc65931 --- /dev/null +++ b/packages/cockpit-benchmark/default.nix @@ -0,0 +1,72 @@ +{ + fio, + jq, + lib, + namespace, + nodejs, + npmHooks, + fetchNpmDeps, + stdenv, +}: +let + inherit (lib.trivial) importJSON; + inherit (lib.${namespace}) mkAllSources selectVariant; + + versionSpec = importJSON ./version.json; + selected = selectVariant versionSpec null null; + sources = mkAllSources selected; + inherit (selected.variables) version; +in +stdenv.mkDerivation (finalAttrs: { + pname = "cockpit-benchmark"; + inherit version; + + src = sources.src; + + npmDeps = fetchNpmDeps { + src = "${finalAttrs.src}/benchmark"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + + nativeBuildInputs = [ + jq + nodejs + npmHooks.npmConfigHook + npmHooks.npmBuildHook + npmHooks.npmInstallHook + ]; + + # fio is the runtime benchmark tool invoked by the plugin's backend script. + passthru.cockpitPath = [ fio ]; + + # npmConfigHook expects to run in the directory with package.json + preConfigure = '' + cd benchmark + ''; + + # Write version.js before vite build (mirrors what the Makefile does) + preBuild = '' + pluginVersion="$(jq -r '.version' ../manifest.json)-$(jq -r '.build_number' ../manifest.json)" + echo "export const pluginVersion = \"''${pluginVersion}\";" > src/version.js + ''; + + npmBuildScript = "build"; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/cockpit/benchmark + cp -r dist/* $out/share/cockpit/benchmark/ + + runHook postInstall + ''; + + meta = { + description = "Cockpit storage benchmark utility using fio"; + homepage = "https://github.com/45Drives/cockpit-benchmark"; + changelog = "https://github.com/45Drives/cockpit-benchmark/releases/tag/v${version}"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.linux; + maintainers = [ ]; + }; +}) diff --git a/packages/cockpit-benchmark/version.json b/packages/cockpit-benchmark/version.json new file mode 100644 index 0000000..b3c8b94 --- /dev/null +++ b/packages/cockpit-benchmark/version.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "variables": { + "version": "2.1.2" + }, + "sources": { + "src": { + "fetcher": "github", + "owner": "45Drives", + "repo": "cockpit-benchmark", + "tag": "v${version}", + "hash": "sha256-YmOCdqAdYPnNcXqCccvI0nVhR/EdYXdkeenmy4DdGK0=" + } + } +} diff --git a/packages/cockpit-machines/default.nix b/packages/cockpit-machines/default.nix new file mode 100644 index 0000000..75948c1 --- /dev/null +++ b/packages/cockpit-machines/default.nix @@ -0,0 +1,82 @@ +{ + lib, + namespace, + nodejs, + stdenv, +}: +let + inherit (lib.trivial) importJSON; + inherit (lib.${namespace}) mkAllSources selectVariant; + + versionSpec = importJSON ./version.json; + selected = selectVariant versionSpec null null; + sources = mkAllSources selected; + inherit (selected.variables) version; +in +stdenv.mkDerivation { + pname = "cockpit-machines"; + inherit version; + + src = sources.src; + + # Pre-vendored node_modules from cockpit-project/node-cache, pinned via the + # node_modules submodule reference in the source tree. + inherit (sources) nodeModules; + + # pkg/lib checked out from the main cockpit repo at the commit pinned in + # the Makefile (COCKPIT_REPO_COMMIT). + inherit (sources) cockpitLib; + + nativeBuildInputs = [ nodejs ]; + + # passthru.cockpitPath is used by the NixOS cockpit module to add runtime + # dependencies to the cockpit service's PATH. + passthru.cockpitPath = [ ]; + + configurePhase = '' + runHook preConfigure + + # Replace the empty git submodule placeholder with the real vendored modules. + # Use dotglob so hidden entries (.bin, .package-lock.json, etc.) are included. + rm -rf node_modules + mkdir node_modules + (shopt -s dotglob; cp -r $nodeModules/* node_modules/) + chmod -R u+w node_modules + + # Node needs package-lock.json at the project root to resolve modules. + cp node_modules/.package-lock.json package-lock.json + + # Wire up pkg/lib from the pinned cockpit repo. + mkdir -p pkg + cp -r $cockpitLib/pkg/lib pkg/lib + chmod -R u+w pkg + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + NODE_ENV=production node build.js + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/cockpit/machines + cp -r dist/* $out/share/cockpit/machines/ + + runHook postInstall + ''; + + meta = { + description = "Cockpit UI for virtual machines (libvirt/KVM)"; + homepage = "https://github.com/cockpit-project/cockpit-machines"; + changelog = "https://github.com/cockpit-project/cockpit-machines/releases/tag/${version}"; + license = lib.licenses.lgpl21Plus; + platforms = lib.platforms.linux; + maintainers = [ ]; + }; +} diff --git a/packages/cockpit-machines/version.json b/packages/cockpit-machines/version.json new file mode 100644 index 0000000..48dd326 --- /dev/null +++ b/packages/cockpit-machines/version.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "variables": { + "version": "350", + "cockpitRepoCommit": "5fb84eaefbc5ff4433a21bc452270af8d09e1ab7", + "nodeCacheRev": "d1c928e67c7891bb05238dde9caa6486bba15625" + }, + "sources": { + "src": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "cockpit-machines", + "tag": "${version}", + "hash": "sha256-jP/ffo0kwju0xxKMHhDwOsNKO7HjOaJZ/uXYijsElNg=" + }, + "nodeModules": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "node-cache", + "rev": "${nodeCacheRev}", + "hash": "sha256-yonEQ7hRskbFDnW/Pc3aOeV7bgu1LCYpi1Ok5/aHeC8=" + }, + "cockpitLib": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "cockpit", + "rev": "${cockpitRepoCommit}", + "hash": "sha256-H9eeTprI0rBKv//1TvGWTtgb5N09/8Audtu5dE1y86o=" + } + } +} diff --git a/packages/cockpit-podman/default.nix b/packages/cockpit-podman/default.nix new file mode 100644 index 0000000..f525d42 --- /dev/null +++ b/packages/cockpit-podman/default.nix @@ -0,0 +1,83 @@ +{ + fetchFromGitHub, + lib, + nodejs, + stdenv, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "cockpit-podman"; + version = "123"; + + src = fetchFromGitHub { + owner = "cockpit-project"; + repo = "cockpit-podman"; + tag = finalAttrs.version; + hash = "sha256-N5nhJU9XUsxLWq3mk3bSyorHEM4zSLHt9I+zkdgU2Vk="; + }; + + # Pre-vendored node_modules from cockpit-project/node-cache, pinned via the + # node_modules submodule reference in the source tree. + nodeModules = fetchFromGitHub { + owner = "cockpit-project"; + repo = "node-cache"; + rev = "e39ef3621b5aefa5bf1c2de7e66a5918fcef620c"; + hash = "sha256-+yhHsGEN1IqIxPY7vQysp1ZczcHzXRoNIVN3DyVgwB8="; + }; + + # pkg/lib is checked out from the main cockpit repo at the pinned commit + # referenced in the Makefile (COCKPIT_REPO_COMMIT). + cockpitLib = fetchFromGitHub { + owner = "cockpit-project"; + repo = "cockpit"; + rev = "5fb84eaefbc5ff4433a21bc452270af8d09e1ab7"; + hash = "sha256-FR6TIKQ+3GuDMOMEivDxEx6E/SVIAXh9Cg36JJ694Wc="; + }; + + nativeBuildInputs = [ nodejs ]; + + # cockpit-podman uses passthru.cockpitPath for the NixOS cockpit module to + # add runtime dependencies to the cockpit service's PATH. + passthru.cockpitPath = [ ]; + + configurePhase = '' + runHook preConfigure + + # Wire up the vendored node_modules + cp -r ${finalAttrs.nodeModules} node_modules + chmod -R u+w node_modules + + # Wire up pkg/lib from the pinned cockpit repo + mkdir -p pkg + cp -r ${finalAttrs.cockpitLib}/pkg/lib pkg/lib + chmod -R u+w pkg + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + NODE_ENV=production node build.js + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/cockpit/podman + cp -r dist/* $out/share/cockpit/podman/ + + runHook postInstall + ''; + + meta = { + description = "Cockpit UI for Podman containers"; + homepage = "https://github.com/cockpit-project/cockpit-podman"; + changelog = "https://github.com/cockpit-project/cockpit-podman/releases/tag/${finalAttrs.version}"; + license = lib.licenses.lgpl21Plus; + platforms = lib.platforms.linux; + maintainers = [ ]; + }; +}) diff --git a/packages/cockpit-podman/version.json b/packages/cockpit-podman/version.json new file mode 100644 index 0000000..24c54a2 --- /dev/null +++ b/packages/cockpit-podman/version.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "variables": { + "version": "123", + "cockpitRepoCommit": "5fb84eaefbc5ff4433a21bc452270af8d09e1ab7", + "nodeCacheRev": "e39ef3621b5aefa5bf1c2de7e66a5918fcef620c" + }, + "sources": { + "src": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "cockpit-podman", + "tag": "${version}", + "hash": "sha256-SkZb8NcN8iSocVpdg4Y8KWNKfyBKn6HhYSO7kIcjxHw=" + }, + "nodeModules": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "node-cache", + "rev": "${nodeCacheRev}", + "hash": "sha256-v+0y2MWvx3NSxEwnuC9GYY0wyRYOV/YnG0a2QbsP/kA=" + }, + "cockpitLib": { + "fetcher": "github", + "owner": "cockpit-project", + "repo": "cockpit", + "rev": "${cockpitRepoCommit}", + "hash": "sha256-H9eeTprI0rBKv//1TvGWTtgb5N09/8Audtu5dE1y86o=" + } + } +}