diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b91f7d5f175a32a49a5ed0788a87146d9b86c3f3..76dce30e2e5137a8a9199f2d739f96db92988406 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ default: docs: stage: "build" script: - - "nix-shell --run 'nix-build docs.nix'" + - "nix-build docs.nix" - "cp --recursive --no-preserve=mode result/docs/. docs/build/" artifacts: paths: @@ -20,10 +20,42 @@ unit-tests: script: - "nix-shell --run 'nix-build nixos/unit-tests.nix' && cat result" +.morph-build: &MORPH_BUILD + stage: "test" + timeout: "3 hours" + + script: + - | + # GRID is set in one of the "instantiations" of this job template. + nix-shell --run "morph build --show-trace morph/grid/${GRID}/grid.nix" + + +morph-build-localdev: + <<: *MORPH_BUILD + variables: + GRID: "local" + + before_script: + - | + # The local grid configuration is *almost* complete enough to build. It + # just needs this tweak. + sed -i 's/undefined/\"unundefined\"/' morph/grid/${GRID}/public-keys/users.nix + +morph-build-testing: + <<: *MORPH_BUILD + variables: + GRID: "testing" + + +morph-build-production: + <<: *MORPH_BUILD + variables: + GRID: "production" + + vulnerability-scan: stage: "test" script: - - "sed -i 's/undefined/\"unundefined\"/' morph/grid/local/secrets/users.nix" - "ci-tools/vulnerability-scan security-report.json" - "ci-tools/count-vulnerabilities <security-report.json" artifacts: @@ -38,24 +70,26 @@ system-tests: script: - "nix-shell --run 'nix-build nixos/system-tests.nix'" -deploy-to-staging: +# A template for a job that can update one of the grids. +.update-grid: &UPDATE_GRID stage: "deploy" + script: | + env --ignore-environment - NIX_PATH=$NIX_PATH GITLAB_USER_LOGIN=$GITLAB_USER_LOGIN CI_JOB_NAME=$CI_JOB_NAME CI_PIPELINE_SOURCE=$CI_PIPELINE_SOURCE CI_COMMIT_BRANCH=$CI_COMMIT_BRANCH ./ci-tools/update-grid-servers "${PRIVATESTORAGEIO_SSH_DEPLOY_KEY_PATH}" "${CI_ENVIRONMENT_NAME}" + +# Update the staging deployment - only on a merge to the staging branch. +update-staging: + <<: *UPDATE_GRID only: - "staging" environment: name: "staging" url: "https://privatestorage-staging.com/" - script: - - echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE " - - echo "and would like to deploy the $CI_COMMIT_BRANCH branch to the $CI_ENVIRONMENT_NAME environment." +# Update the production deployment - only on a merge to the production branch. deploy-to-production: - stage: "deploy" + <<: *UPDATE_GRID only: - "production" environment: name: "production" url: "https://privatestorage.io/" - script: - - echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE " - - echo "and would like to deploy the $CI_COMMIT_BRANCH branch to the $CI_ENVIRONMENT_NAME environment." diff --git a/ci-tools/count-vulnerabilities b/ci-tools/count-vulnerabilities index 9db1c5e7e3aa756dc5b151fbcc30bc4572dd1eba..b1d2b804d81c9c53c0f9a9b41e4e554978c0032d 100755 --- a/ci-tools/count-vulnerabilities +++ b/ci-tools/count-vulnerabilities @@ -1,4 +1,5 @@ -#!/usr/bin/env python3 +#! /usr/bin/env nix-shell +#! nix-shell -i python3 -p python3 from sys import stdin from json import load diff --git a/ci-tools/known_hosts.production b/ci-tools/known_hosts.production new file mode 100644 index 0000000000000000000000000000000000000000..88e5696c47a6a5583e0eb20e3c77f82bc46bda8e --- /dev/null +++ b/ci-tools/known_hosts.production @@ -0,0 +1,7 @@ +monitoring.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGvKv2y+IAL4+oDnX7Cm5G9QuADBHUj9OxzLX0okf6hF +payments.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBlyFTJyN+VDlzGWANKqBlXeexlX/xTpp6gb5sUlA9U +storage001.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILQojjGVvmjZfDcrlec8ZmpbzMEeHd4+t4DJq1R/NUXw +storage002.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKDWqK7FBzT4L1eoIU/iaEZNZxq3Jr613PmK2nbAXFs2 +storage003.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHobJpQVv9GaTv8Xh9CGlL7BL5yKLxCiD3ZDdVTyt0Ep +storage004.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy96VEPp617ewxdkt+8ZgWcYkLxlVG/C7bZAq0ULH+z +storage005.private.storage ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKOgJBoER0lX2Rx8UIfv/3MVJXNFn9RldYmpU+EqAc9H diff --git a/ci-tools/known_hosts.staging b/ci-tools/known_hosts.staging new file mode 100644 index 0000000000000000000000000000000000000000..2a015656e4c21498f2fe152723888046d9341c1a --- /dev/null +++ b/ci-tools/known_hosts.staging @@ -0,0 +1,3 @@ +monitoring.privatestorage-staging.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINI9kvEBaOMvpWqcFH+6nFvRriBECKB4RFShdPiIMkk9 +payments.privatestorage-staging.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0eO/01VFwdoZzpclrmu656eaMkE19BaxtDdkkFHMa8 +storage001.privatestorage-staging.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFP8L6OHCxq9XFd8ME8ZrCbmO5dGZDPH8I5dm0AwSGiN diff --git a/ci-tools/update-grid-servers b/ci-tools/update-grid-servers new file mode 100755 index 0000000000000000000000000000000000000000..c5206fd481096cc8eddb2a2c313cff7d92fe2db6 --- /dev/null +++ b/ci-tools/update-grid-servers @@ -0,0 +1,132 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p jp nix openssh + +# +# Tell all servers belonging to a certain grid that they should update +# themselves to the latest configuration associated with that grid. +# + +set -euxo pipefail + +# Find the location of this script so we can refer to data files with a known +# relative location. +HERE=$(dirname $0) + +# Get the path to the ssh key which authorizes us to deliver this +# notification. This path contains a base64-encoded key because of +# limitations placed on the values of GitLab job environment variables. We'll +# decode it later. +ENCODED_DEPLOY_KEY_PATH=$1 +shift + +# Get the name of the grid to which we're going to deliver notification. This +# corresponds to the name of one of the directories in the top-level `morph` +# directory. +GRIDNAME=$1 +shift + +# Tell one server to update itself. +update_one_node() { + grid_name=$1 + shift + + deploy_key_path=$1 + shift + + node=$1 + shift + + # Avoid both the "host key unknown" prompt and the possibility for a + # man-in-the-middle attack (on every single deploy!) by referring to a + # pre-initialized known hosts file for this grid. + # + # Then use the specified deploy key to authenticate as the deployment user + # and trigger the update on the host. There's no command here because the + # deployment key is restricted *only* the deloyment update command and the + # ssh server will supply that command itself. + ssh -o "UserKnownHostsFile=${HERE}/known_hosts.${grid_name}" -i "${deploy_key_path}" "deployment@${node}" +} + +# Tell all servers belonging to one grid to update themselves. +update_grid_nodes() { + deploy_key_path=$1 + shift + + gridname=$1 + shift + + case "$gridname" in + "production") + grid_dir=./morph/grid/production + domain=private.storage + ;; + + "staging") + grid_dir=./morph/grid/testing + domain=privatestorage-staging.com + ;; + + *) + echo "Unknown grid: ${gridname}" + exit 1 + esac + + # Find the names of all hosts that belong to this grid. This list includes + # one extra string, "network", which is morph configuration stuff and we need + # to filter out later. + nodes=$(nix eval --json "(builtins.concatStringsSep \" \" (builtins.attrNames (import $grid_dir/grid.nix)))" | jp --unquoted @) + + # Tell every server in the network to update itself. + for node in ${nodes}; do + if [ "${node}" = "network" ]; then + # This isn't a server, it's part of the morph configuration. + continue + fi + update_one_node "${gridname}" "${deploy_key_path}" "${node}.${domain}" + done +} + +decode_deploy_key() { + encoded_key_path=$1 + shift + + # Make sure the deploy key file is not readable by anyone else. Not + # that there should be anyone else looking - but OpenSSH won't even read + # it if it looks like it is too open. + umask 077 + + # Make up a safe-ish place on the filesystem to write the key. + decoded_key_path="$(mktemp -d)/deploy_key" + + # Decode the contents of the encoded key path into a decoded key path. + base64 --decode "${encoded_key_path}" > "${decoded_key_path}" + + # If OpenSSH doesn't find a newline after the last line of the key + # material then it fails to parse it. So, make sure there is one. If + # there was already one, it's fine to have an extra. + echo >> "${decoded_key_path}" + + # Remove the key from the filesystem to reduce the chance of unintentional + # disclosure. Overall our handling of this key is still not *particulary* + # safe or secure but that's why the key is only authorized to perform a + # single very specific operation. + rm -v "${encoded_key_path}" >/dev/stderr + + echo -n "${decoded_key_path}" +} + +# Announce our intentions. +show_banner() { + echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE" + echo "and I am deploying the $CI_COMMIT_BRANCH branch to the $GRIDNAME environment." +} + +show_banner + +DEPLOY_KEY_PATH="$(decode_deploy_key "${ENCODED_DEPLOY_KEY_PATH}")" + +# Update the deployment +update_grid_nodes "${DEPLOY_KEY_PATH}" "${GRIDNAME}" + +# Remove the decoded key from the filesystem as well. +rm -v "${DEPLOY_KEY_PATH}" diff --git a/ci-tools/vulnerability-scan b/ci-tools/vulnerability-scan index 3162e49511697ed0ea13e0121a67336405ce5225..48bf51e071a398f37565717a22b2066d3f905fbe 100755 --- a/ci-tools/vulnerability-scan +++ b/ci-tools/vulnerability-scan @@ -21,7 +21,7 @@ OUTPUT=$1 [ -e scan-target ] && rm -v scan-target nix-shell --run ' set -x -if morph_result=$(morph build morph/grid/local/grid.nix 2>&1); then +if morph_result=$(morph build morph/grid/testing/grid.nix 2>&1); then object=$(echo "$morph_result" | tail -n 1) ln -s "$object" scan-target else diff --git a/docs.nix b/docs.nix index 813a6cb432942fccd96b96ee07313ff84cf885c6..4c8b230a7eddb462bf47a4c3ee591e64fb3ce1ff 100644 --- a/docs.nix +++ b/docs.nix @@ -1,14 +1,2 @@ -{ pkgs ? import <nixpkgs> { } }: -let - # NixOS 19.03 packaged graphviz has trouble rendering our architecture - # overview. Latest from upstream does alright, though. Use that. - make-graphviz = (import (pkgs.path + /pkgs/tools/graphics/graphviz/base.nix) { - rev = "b29d8e369011b832f72e0d250a05a0a15dcb5daa"; - sha256 = "1w61filywn9cif2nryf6vd34mxxbvv25q34fd34am1rx70bk08ps"; - version = "b29d8e369011b832f72e0d250a05a0a15dcb5daa"; - }); - graphviz = (pkgs.callPackage make-graphviz { }).overrideAttrs (old: { - patches = []; - }); -in - pkgs.callPackage ./privatestorageio.nix { inherit graphviz; } +{ pkgs ? import ./nixpkgs-2105.nix { } }: +pkgs.callPackage ./privatestorageio.nix { } diff --git a/docs/source/ops/generating-keys.rst b/docs/source/ops/generating-keys.rst index 47a1f4e91a876ac1919252c099654886f0bd128a..c2f7028f2bc263c9e5bac40f78ca0adfb4861415 100644 --- a/docs/source/ops/generating-keys.rst +++ b/docs/source/ops/generating-keys.rst @@ -1,9 +1,10 @@ Generating keys =============== -There's an example ``secrets`` repo in ``morph/grid/local/secrets``. +There are example ``public-keys`` and ``private-keys`` repos in ``morph/grid/local/``. ``<grid>/config.json`` has the paths for the key files for the respective grid. -Create a symlink named ``secrets`` to your secret key repository for the deployment you are working on. +Create a symlink ``private-keys`` to your secret key repositories for the deployment you are working on. +Create a directory named ``public-keys`` containing the corresponding public keys for the deployment. Stripe @@ -55,22 +56,6 @@ Move the three .pem files into the payment's server ``/var/lib/letsencrypt/live/ Monitoring VPN `````````````` -Create Wireguard VPN key pairs in ``secrets/monitoringvpn/`` or where you have them. - -``tools/create-vpn-keys.sh`` holds a script to rotate all VPN keys at once:: +Create all of the Wireguard VPN keys for a grid:: ./tools/create-vpn-keys.sh morph/grid/testing/grid.nix - -Or do it manually:: - - cd secrets/monitoringvpn - for i in 1 11 12 13 ; do - wg genkey | tee 172.23.23.${i}.key | wg pubkey > 172.23.23.${i}.pub - done - - ln -s 172.23.23.1.key server.key - ln -s 172.23.23.1.pub server.pub - -And a shared VPN key for "post-quantum resistance":: - - wg genpsk > preshared.key diff --git a/morph/README.rst b/morph/README.rst index 12472518ad8e061764d6812694c306e87553c843..96d03eb3cf522af6f1b0065105a2d57ab5c78f6a 100644 --- a/morph/README.rst +++ b/morph/README.rst @@ -42,8 +42,8 @@ grid Specific grid definitions live in subdirectories beneath this directory. -secrets -~~~~~~~ +private-keys +~~~~~~~~~~~~ This must be created and populated before the grid can be built or deployed. @@ -55,10 +55,44 @@ This path is **ignored** by git. The intended workflow is that the secrets will be maintained on secure storage and a symlink to the correct location created here. This keeps the secrets themselves out of the git working tree as an extra protection against unintentionally committing them. -An exception is the ``secrets`` directory in the ``local`` morph grid: +An exception is the ``private-keys`` directory in the ``local`` morph grid: That directory is fully populated, provided as an example, and mostly: not very secret. Do not deploy these keys to machines reachable via the internet. +Strictly speaking, +this path is configurable in the grid's ``config.json`` but all three grids currently use this name. + +public-keys +~~~~~~~~~~~ + +This must be created and populated before the grid can be built or deployed. + +This directory contains any public key material necessary for operation of the grid. +This includes the public keys corresponding to any private keys held in ``private-keys``. + +As for ``private-keys``, +this path can be configured in the grid's ``config.json``. + +Star-crossed Keys +^^^^^^^^^^^^^^^^^ + +Where the system uses keypairs, +the public and private parts of those keypairs are stored in different locations +(``public-keys`` and ``private-keys`` mentioned above). +This somewhat complicates key management because any key rotation involves changing key material in two location instead of just one. + +This complication is balanced against a specific operational goal: +that our build systems operate without copies of our private keys. +Our system configurations do currently have build-time dependencies on public keys. + +Splitting public keys and private keys across two different storage locations provides a simple mechanism for providing build systems with the public keys but withholding the private keys. + +In the future we may: +* be sufficiently confident in the security of our build systems to let them have our private keys; or +* remove the dependency upon public keys from the build process. + +Either of these directions would let us re-unify public/private-key storage and remove this complication. + config.json ~~~~~~~~~~~ diff --git a/morph/grid/local/README.rst b/morph/grid/local/README.rst index 345547244635734278aa76cb5cd59946f2afd37f..d30d8766a4ef5a8db228ef38374330734e69cba7 100644 --- a/morph/grid/local/README.rst +++ b/morph/grid/local/README.rst @@ -33,18 +33,11 @@ Use the local development environment install -d ~/.ssh ; vagrant ssh-config >> ~/.ssh/config -5. Edit the generated configuration: Add the ``publicIP`` addresses from ``grid.nix`` to ssh config **Host** match blocks (**not** HostName) so the ``Host`` lines all read like:: - - Host payments 192.168.67.21 - HostName 127.0.0.1 - User vagrant - [...] - Latest Morph honors the ``SSH_CONFIG_FILE`` environment variable (`since 3f90aa88 (March 2020, v 1.5.0) <https://github.com/DBCDK/morph/commit/3f90aa885fac1c29fce9242452fa7c0c505744ef#diff-d155ad793bd62e6ea4c44ba985049ecb13a4f4f32f799791b2bce695a16c0101>`_), so in the future this should get a bit more convenient. 6. Add your SSH key to ``users.nix`` so you'll be able to log in after deploying the new configuration:: - $EDITOR secrets/users.nix + $EDITOR public-keys/users.nix 7. Then, build and deploy our software to the Vagrant VMs:: @@ -56,4 +49,3 @@ Use the local development environment morph upload-secrets grid.nix You should now be able to log in with the users and keys you set in your ``users.nix`` file. - diff --git a/morph/grid/local/Vagrantfile b/morph/grid/local/Vagrantfile index 7ad95ca872a72e5da6c11b3269e2a824cf8a55f9..a871cbbe72e410de88a19596ff528391e32ff811 100644 --- a/morph/grid/local/Vagrantfile +++ b/morph/grid/local/Vagrantfile @@ -8,19 +8,29 @@ Vagrant.configure("2") do |config| # For a complete reference, please see the online documentation at # https://docs.vagrantup.com. - config.vm.define "payments" do |config| + config.vm.define "payments.localdev" do |config| config.vm.hostname = "payments" config.vm.box = "esselius/nixos" config.vm.box_version = "20.09" config.vm.box_check_update = false + + # To use the self-updating deployment system you need more memory. Giving + # all of the VMs enough memory for this is rather taxing, though, and the + # self-updating deployment system is not particularly useful for local + # dev. But should you want to: + # + # config.vm.provider "virtualbox" do |v| + # v.memory = 4096 + # end + config.vm.network "private_network", ip: "192.168.67.21" # Add self signed SSL key for zkap-issuer: - config.vm.provision "file", source: "secrets/payments-localdev-ssl", destination: "/tmp/payments-localdev-ssl" + config.vm.provision "file", source: "private-keys/payments-localdev-ssl", destination: "/tmp/payments-localdev-ssl" config.vm.provision "shell", inline: "sudo mkdir -p /var/lib/letsencrypt/live/payments.localdev/" config.vm.provision "shell", inline: "sudo mv /tmp/payments-localdev-ssl/* /var/lib/letsencrypt/live/payments.localdev/" end - config.vm.define "storage1" do |config| + config.vm.define "storage1.localdev" do |config| config.vm.hostname = "storage1" config.vm.box = "esselius/nixos" config.vm.box_version = "20.09" @@ -28,7 +38,7 @@ Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.67.22" end - config.vm.define "storage2" do |config| + config.vm.define "storage2.localdev" do |config| config.vm.hostname = "storage2" config.vm.box = "esselius/nixos" config.vm.box_version = "20.09" @@ -36,7 +46,7 @@ Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.67.23" end - config.vm.define "monitoring" do |config| + config.vm.define "monitoring.localdev" do |config| config.vm.hostname = "monitoring" config.vm.box = "esselius/nixos" config.vm.box_version = "20.09" @@ -54,4 +64,3 @@ Vagrant.configure("2") do |config| end end - diff --git a/morph/grid/local/config.json b/morph/grid/local/config.json index 38f00367bf2fa36ad7663c89f7849146783b8515..8b23b6f1152be4fa94e8935342bf11f7706d036c 100644 --- a/morph/grid/local/config.json +++ b/morph/grid/local/config.json @@ -1,7 +1,7 @@ -{ "publicStoragePort": 8898 -, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key" -, "stripeSecretKeyPath": "./secrets/stripe.secret" -, "monitoringvpnKeyDir": "./secrets/monitoringvpn" +{ "domain": "localdev" +, "publicStoragePort": 8898 +, "publicKeyPath": "./public-keys" +, "privateKeyPath": "./private-keys" , "monitoringvpnEndpoint": "192.168.67.24:51820" , "passValue": 1000000 , "issuerDomains": ["payments.localdev"] @@ -9,4 +9,5 @@ , "allowedChargeOrigins": [ "http://localhost:5000" ] +, "monitoringGoogleOAuthClientID": "" } diff --git a/morph/grid/local/grid.nix b/morph/grid/local/grid.nix index fdc0cde55be4f1b644c212ce20f6c3e44af8e3df..3def2d77556e8b82b5fd0dbd2513f3d08b7ea2c7 100644 --- a/morph/grid/local/grid.nix +++ b/morph/grid/local/grid.nix @@ -1,64 +1,89 @@ -# Load the helper function and call it with arguments tailored for the local -# grid. It will make the morph configuration for us. We share this function -# with the production grid and have one fewer possible point of divergence. -import ../../lib/make-grid.nix { - name = "LocalDev"; - config = ./config.json; - nodes = cfg: - let - sshUsers = import ./secrets/users.nix; +let + pkgs = import <nixpkgs> { }; - # Get absolute vpn key directory path, as a string: - monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}"; + gridlib = import ../../lib; + rawConfig = pkgs.lib.trivial.importJSON ./config.json; + config = rawConfig // { + sshUsers = import ./public-keys/users.nix; - # TBD: derive these automatically: - hostsMap = { - "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; - "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; - "172.23.23.12" = [ "storage1" "storage1.monitoringvpn" ]; - "172.23.23.13" = [ "storage2" "storage2.monitoringvpn" ]; + # Convert relative paths to absolute so library code can resolve names + # correctly. + publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; + privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; + }; + + # Configure deployment management authorization for all systems in the grid. + deployment = { + services.private-storage.deployment = { + authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + gridName = "local"; }; - vpnClientIPs = [ "172.23.23.11" "172.23.23.12" "172.23.23.13" ]; - nodeExporterTargets = [ "monitoring" "payments" "storage1" "storage2" ]; + }; + + payments = { + imports = [ + gridlib.issuer + (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.21"; })) + (gridlib.customize-issuer (config // { + monitoringvpnIPv4 = "172.23.23.11"; + })) + deployment + ]; + }; - in { - "payments" = import ../../lib/make-issuer.nix (cfg // rec { - publicIPv4 = "192.168.67.21"; - monitoringvpnIPv4 = "172.23.23.11"; - hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; }); - stateVersion = "19.03"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - }); + storage1 = { + imports = [ + gridlib.storage + (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.22"; })) + (gridlib.customize-storage (config // { + monitoringvpnIPv4 = "172.23.23.12"; + stateVersion = "19.09"; + })) + deployment + ]; + }; - "storage1" = import ../../lib/make-testing.nix (cfg // rec { - publicIPv4 = "192.168.67.22"; - monitoringvpnIPv4 = "172.23.23.12"; - hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; }); - stateVersion = "19.09"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - }); + storage2 = { + imports = [ + gridlib.storage + (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.23"; })) + (gridlib.customize-storage (config // { + monitoringvpnIPv4 = "172.23.23.13"; + stateVersion = "19.09"; + })) + deployment + ]; + }; - "storage2" = import ../../lib/make-testing.nix (cfg // rec { - publicIPv4 = "192.168.67.23"; - monitoringvpnIPv4 = "172.23.23.13"; - hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; }); - stateVersion = "19.09"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - }); + monitoring = { + imports = [ + gridlib.monitoring + (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.24"; })) + (gridlib.customize-monitoring { + inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; + inherit (config) domain publicKeyPath privateKeyPath letsEncryptAdminEmail; + googleOAuthClientID = config.monitoringGoogleOAuthClientID; + monitoringvpnIPv4 = "172.23.23.1"; + stateVersion = "19.09"; + }) + deployment + ]; + }; + + # TBD: derive these automatically: + hostsMap = { + "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; + "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; + "172.23.23.12" = [ "storage1" "storage1.monitoringvpn" ]; + "172.23.23.13" = [ "storage2" "storage2.monitoringvpn" ]; + }; + vpnClientIPs = [ "172.23.23.11" "172.23.23.12" "172.23.23.13" ]; + nodeExporterTargets = [ "monitoring" "payments" "storage1" "storage2" ]; + paymentExporterTargets = [ "payments" ]; - "monitoring" = import ../../lib/make-monitoring.nix (cfg // rec { - publicIPv4 = "192.168.67.24"; - monitoringvpnIPv4 = "172.23.23.1"; - inherit vpnClientIPs; - inherit hostsMap; - inherit nodeExporterTargets; - hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; }); - stateVersion = "19.09"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - }); +in { + network = { + description = "PrivateStorage.io LocalDev Grid"; }; + inherit payments monitoring storage1 storage2; } diff --git a/morph/grid/local/private-keys/README.rst b/morph/grid/local/private-keys/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..684bf942a8010129f49cfcf79f5df1b60965ae45 --- /dev/null +++ b/morph/grid/local/private-keys/README.rst @@ -0,0 +1,44 @@ +Deployment Secrets +================== + +Deploying PrivateStorageio requires certain secrets. +For the localdev grid these secrets are kept in this (public) directory. +This is intended to help make it as easy as possible to launch a local deployment. +It also serves as an example of what secrets are required for any other deployment. + +You can find more information about some of these secrets in ``ops/generating-keys.rst``. + +deploy_key +---------- + +This is an SSH private key which will be authorized to trigger a deployment update on the deployment hosts themselves. +The corresponding SSH public key is kept in the ``public-keys`` location. + +grafana-admin.password +---------------------- + +This is the initial admin password for the Grafana web admin on the monitoring host. + +stripe.secret +------------- + +This is the Stripe secret key which the payment server uses to finalize payment processing using Stripe. +The corresponding Stripe public key is kept in the ``public-keys`` location. + +ristretto.signing-key +--------------------- + +This is the Ristretto-group private key used by the ZKAP issuer. + +monitoringvpn +------------- + +This directory holds Wireguard private keys for each of the hosts so they can participate in the deployment VPN. +The corresponding public keys are kept in the ``public-keys`` location. + +payments-localdev-ssl +--------------------- + +This secret is *only* present for the localdev grid. +This contains a TLS certificate and private key for the payment server. +Other deployments will automatically generate a key and obtain a certificate from Let's Encrypt. diff --git a/morph/grid/local/private-keys/deploy_key b/morph/grid/local/private-keys/deploy_key new file mode 100644 index 0000000000000000000000000000000000000000..5c8807906eca2a818d2807e2512d03642d038edb --- /dev/null +++ b/morph/grid/local/private-keys/deploy_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACADU1IBThyH0blWBG8afIA/h/bmVdUQkFAuAIQgAWE+ewAAAJjsA+8c7APv +HAAAAAtzc2gtZWQyNTUxOQAAACADU1IBThyH0blWBG8afIA/h/bmVdUQkFAuAIQgAWE+ew +AAAED6aLiQi/K2qG8sLsvV8Xar9PjJeFxKfb+GUvmseu8TqQNTUgFOHIfRuVYEbxp8gD+H +9uZV1RCQUC4AhCABYT57AAAADmV4YXJrdW5AYmFyeW9uAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/morph/grid/local/private-keys/grafana-admin.password b/morph/grid/local/private-keys/grafana-admin.password new file mode 100644 index 0000000000000000000000000000000000000000..a31f068f733c21aa08fe347fbd6780397c65541c --- /dev/null +++ b/morph/grid/local/private-keys/grafana-admin.password @@ -0,0 +1 @@ +Naht3Pha diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.11.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.11.key similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.11.key rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.11.key diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.12.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.12.key similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.12.key rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.12.key diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.13.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.13.key similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.13.key rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.13.key diff --git a/morph/grid/local/secrets/monitoringvpn/preshared.key b/morph/grid/local/private-keys/monitoringvpn/preshared.key similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/preshared.key rename to morph/grid/local/private-keys/monitoringvpn/preshared.key diff --git a/morph/grid/local/secrets/monitoringvpn/server.key b/morph/grid/local/private-keys/monitoringvpn/server.key similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/server.key rename to morph/grid/local/private-keys/monitoringvpn/server.key diff --git a/morph/grid/local/secrets/payments-localdev-ssl/cert.pem b/morph/grid/local/private-keys/payments-localdev-ssl/cert.pem similarity index 100% rename from morph/grid/local/secrets/payments-localdev-ssl/cert.pem rename to morph/grid/local/private-keys/payments-localdev-ssl/cert.pem diff --git a/morph/grid/local/secrets/payments-localdev-ssl/chain.pem b/morph/grid/local/private-keys/payments-localdev-ssl/chain.pem similarity index 100% rename from morph/grid/local/secrets/payments-localdev-ssl/chain.pem rename to morph/grid/local/private-keys/payments-localdev-ssl/chain.pem diff --git a/morph/grid/local/secrets/payments-localdev-ssl/privkey.pem b/morph/grid/local/private-keys/payments-localdev-ssl/privkey.pem similarity index 100% rename from morph/grid/local/secrets/payments-localdev-ssl/privkey.pem rename to morph/grid/local/private-keys/payments-localdev-ssl/privkey.pem diff --git a/morph/grid/local/secrets/ristretto.signing-key b/morph/grid/local/private-keys/ristretto.signing-key similarity index 100% rename from morph/grid/local/secrets/ristretto.signing-key rename to morph/grid/local/private-keys/ristretto.signing-key diff --git a/morph/grid/local/secrets/stripe.secret b/morph/grid/local/private-keys/stripe.secret similarity index 100% rename from morph/grid/local/secrets/stripe.secret rename to morph/grid/local/private-keys/stripe.secret diff --git a/morph/grid/local/public-keys/deploy_key.pub b/morph/grid/local/public-keys/deploy_key.pub new file mode 100644 index 0000000000000000000000000000000000000000..15d38cefef61e5f2008efa949ff36245677f8426 --- /dev/null +++ b/morph/grid/local/public-keys/deploy_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIANTUgFOHIfRuVYEbxp8gD+H9uZV1RCQUC4AhCABYT57 exarkun@baryon diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.11.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.11.pub similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.11.pub rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.11.pub diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.12.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.12.pub similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.12.pub rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.12.pub diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.13.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.13.pub similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/172.23.23.13.pub rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.13.pub diff --git a/morph/grid/local/secrets/monitoringvpn/server.pub b/morph/grid/local/public-keys/monitoringvpn/server.pub similarity index 100% rename from morph/grid/local/secrets/monitoringvpn/server.pub rename to morph/grid/local/public-keys/monitoringvpn/server.pub diff --git a/morph/grid/local/secrets/users.nix b/morph/grid/local/public-keys/users.nix similarity index 83% rename from morph/grid/local/secrets/users.nix rename to morph/grid/local/public-keys/users.nix index 93a8b660c78fa12b1e20c6d560f78efb1b5684c7..412077c0d5d6d98024036e369dfa552604f2dc57 100644 --- a/morph/grid/local/secrets/users.nix +++ b/morph/grid/local/public-keys/users.nix @@ -1,4 +1,4 @@ -# Add your public key. Example: +# Add your public key. Example: # let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHx7wJQNqKn8jOC4AxySRL2UxidNp7uIK9ad3pMb1ifF flo@fs-la"; let key = undefined; in { "root" = key; "vagrant" = key; } diff --git a/morph/grid/production/.gitignore b/morph/grid/production/.gitignore index db2fc0de62d01d6d7eec83f8f3e8c3b13b20392a..e3b6111c86090b06c38b9e5afd1fcd16838ddf47 100644 --- a/morph/grid/production/.gitignore +++ b/morph/grid/production/.gitignore @@ -1 +1 @@ -secrets +private-keys diff --git a/morph/grid/production/config.json b/morph/grid/production/config.json index ef7dc53649febcd7beb7901bb3608204df197059..fcae1563a8fc0d3a8a11324fc6667105ae3179c8 100644 --- a/morph/grid/production/config.json +++ b/morph/grid/production/config.json @@ -1,7 +1,7 @@ -{ "publicStoragePort": 8898 -, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key" -, "stripeSecretKeyPath": "./secrets/stripe.secret" -, "monitoringvpnKeyDir": "./secrets/monitoringvpn" +{ "domain": "private.storage" +, "publicStoragePort": 8898 +, "privateKeyPath": "./private-keys" +, "publicKeyPath": "./public-keys" , "monitoringvpnEndpoint": "monitoring.private.storage:51820" , "passValue": 1000000 , "issuerDomains": [ @@ -15,4 +15,5 @@ , "https://private.storage" , "https://www.private.storage" ] +, "monitoringGoogleOAuthClientID": "802959152038-klpkk38sfnqmknn1ucg7pvs4hcc2k8ae.apps.googleusercontent.com" } diff --git a/morph/grid/production/grid.nix b/morph/grid/production/grid.nix index fee0c9be6faed47d4a702b5b53c2419cbb677ba6..e663d2243e4aa6078260e41f07f807f606e64ef6 100644 --- a/morph/grid/production/grid.nix +++ b/morph/grid/production/grid.nix @@ -1,117 +1,128 @@ -# Load the helper function and call it with arguments tailored for the testing -# grid. It will make the morph configuration for us. We share this function -# with the testing grid and have one fewer possible point of divergence. -import ../../lib/make-grid.nix { - name = "Production"; - config = ./config.json; - nodes = cfg: - let - sshUsers = import ./secrets/users.nix; +# See morph/grid/local/grid.nix for additional commentary. +let + pkgs = import <nixpkgs> { }; - # Get absolute vpn key directory path, as a string: - monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}"; + gridlib = import ../../lib; + rawConfig = pkgs.lib.trivial.importJSON ./config.json; + config = rawConfig // { + sshUsers = import ./public-keys/users.nix; - # TBD: derive these automatically: - hostsMap = { - "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; - "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; - "172.23.23.21" = [ "storage001" "storage001.monitoringvpn" ]; - "172.23.23.22" = [ "storage002" "storage002.monitoringvpn" ]; - "172.23.23.23" = [ "storage003" "storage003.monitoringvpn" ]; - "172.23.23.24" = [ "storage004" "storage004.monitoringvpn" ]; - "172.23.23.25" = [ "storage005" "storage005.monitoringvpn" ]; - }; - vpnClientIPs = [ - "172.23.23.11" - "172.23.23.21" - "172.23.23.22" - "172.23.23.23" - "172.23.23.24" - "172.23.23.25" - ]; - nodeExporterTargets = [ - "monitoring" - "payments" - "storage001" - "storage002" - "storage003" - "storage004" - "storage005" - ]; + # Convert relative paths to absolute so library code can resolve names + # correctly. + publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; + privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; + }; - in { - # Here are the hosts that are in this morph network. This is sort of like - # a server manifest. We try to keep as many of the specific details as - # possible out of *this* file so that this file only grows as server count - # grows. If it grows too much, we can load servers by listing contents of - # a directory or reading from another JSON file or some such. For now, - # I'm just manually maintaining these entries. - # - # The name on the left of the `=` is mostly irrelevant but it does provide - # a default hostname for the server if the configuration on the right side - # doesn't specify one. - # - # The names must be unique! - "payments.privatestorage.io" = import ../../lib/make-issuer.nix (cfg // { - publicIPv4 = "18.184.142.208"; - monitoringvpnIPv4 = "172.23.23.11"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - hardware = ../../lib/issuer-aws.nix; - stateVersion = "19.03"; - }); + # Configure deployment management authorization for all systems in the grid. + deployment = { + services.private-storage.deployment = { + authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + gridName = "production"; + }; + }; - "storage001" = import ../../lib/make-storage.nix (cfg // { - cfg = import ./storage001-config.nix; - inherit sshUsers; - hardware = ./storage001-hardware.nix; - stateVersion = "19.09"; - monitoringvpnIPv4 = "172.23.23.21"; - inherit monitoringvpnKeyDir; - }); - "storage002" = import ../../lib/make-storage.nix (cfg // { - cfg = import ./storage002-config.nix; - inherit sshUsers; - hardware = ./storage002-hardware.nix; - stateVersion = "19.09"; - monitoringvpnIPv4 = "172.23.23.22"; - inherit monitoringvpnKeyDir; - }); - "storage003" = import ../../lib/make-storage.nix (cfg // { - cfg = import ./storage003-config.nix; - inherit sshUsers; - hardware = ./storage003-hardware.nix; - stateVersion = "19.09"; - monitoringvpnIPv4 = "172.23.23.23"; - inherit monitoringvpnKeyDir; - }); - "storage004" = import ../../lib/make-storage.nix (cfg // { - cfg = import ./storage004-config.nix; - inherit sshUsers; - hardware = ./storage004-hardware.nix; + payments = { + imports = [ + gridlib.issuer + gridlib.hardware-aws + (gridlib.customize-issuer (config // { + monitoringvpnIPv4 = "172.23.23.11"; + })) + deployment + ]; + }; + + monitoring = { + imports = [ + gridlib.monitoring + gridlib.hardware-aws + (gridlib.customize-monitoring { + inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; + inherit (config) domain publicKeyPath privateKeyPath letsEncryptAdminEmail; + googleOAuthClientID = config.monitoringGoogleOAuthClientID; + monitoringvpnIPv4 = "172.23.23.1"; stateVersion = "19.09"; - monitoringvpnIPv4 = "172.23.23.24"; - inherit monitoringvpnKeyDir; - }); - "storage005" = import ../../lib/make-storage.nix (cfg // { - cfg = import ./storage005-config.nix; - inherit sshUsers; - hardware = ./storage005-hardware.nix; - stateVersion = "19.03"; - monitoringvpnIPv4 = "172.23.23.25"; - inherit monitoringvpnKeyDir; - }); + }) + deployment + ]; + }; + + defineStorageNode = name: { vpnIP, stateVersion }: + let + nodecfg = import "${./.}/${name}-config.nix"; + hardware ="${./.}/${name}-hardware.nix"; + in { + imports = [ + # Get some of the very lowest-level system configuration for this + # node. This isn't all *completely* hardware related. Maybe some + # more factoring is in order, someday. + hardware + + # Slightly awkwardly, enable some of our hardware / network / bootloader options. + ../../../nixos/modules/100tb.nix + + # Get all of the configuration that is common across all storage nodes. + gridlib.storage + + # Then customize the storage system a little bit based on this node's particulars. + (gridlib.customize-storage (config // nodecfg // { + monitoringvpnIPv4 = vpnIP; + inherit stateVersion; + })) + + # Also configure deployment management authorization + deployment + ]; + + # And supply configuration for those hardware / network / bootloader + # options. See the 100tb module for handling of this value. The module + # name is quoted because `1` makes `100tb` look an awful lot like a + # number. + "100tb".config = nodecfg; + }; + + # Define all of the storage nodes for this grid. + storageNodes = builtins.mapAttrs defineStorageNode { + storage001 = { vpnIP = "172.23.23.21"; stateVersion = "19.09"; }; + storage002 = { vpnIP = "172.23.23.22"; stateVersion = "19.09"; }; + storage003 = { vpnIP = "172.23.23.23"; stateVersion = "19.09"; }; + storage004 = { vpnIP = "172.23.23.24"; stateVersion = "19.09"; }; + storage005 = { vpnIP = "172.23.23.25"; stateVersion = "19.03"; }; + }; + + # TBD: derive these automatically: + hostsMap = { + "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; + "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; + "172.23.23.21" = [ "storage001" "storage001.monitoringvpn" ]; + "172.23.23.22" = [ "storage002" "storage002.monitoringvpn" ]; + "172.23.23.23" = [ "storage003" "storage003.monitoringvpn" ]; + "172.23.23.24" = [ "storage004" "storage004.monitoringvpn" ]; + "172.23.23.25" = [ "storage005" "storage005.monitoringvpn" ]; + }; + vpnClientIPs = [ + "172.23.23.11" + "172.23.23.21" + "172.23.23.22" + "172.23.23.23" + "172.23.23.24" + "172.23.23.25" + ]; + nodeExporterTargets = [ + "monitoring" + "payments" + "storage001" + "storage002" + "storage003" + "storage004" + "storage005" + ]; + paymentExporterTargets = [ "payments" ]; - "monitoring" = import ../../lib/make-monitoring.nix (cfg // { - publicIPv4 = "monitoring.private.storage"; - monitoringvpnIPv4 = "172.23.23.1"; - inherit monitoringvpnKeyDir; - inherit vpnClientIPs; - inherit hostsMap; - inherit nodeExporterTargets; - hardware = ../../lib/issuer-aws.nix; - stateVersion = "19.09"; - inherit sshUsers; - }); +in { + network = { + description = "PrivateStorage.io Production Grid"; }; -} + inherit payments; + inherit monitoring; +} // storageNodes diff --git a/morph/grid/production/public-keys/deploy_key.pub b/morph/grid/production/public-keys/deploy_key.pub new file mode 100644 index 0000000000000000000000000000000000000000..3d9ea022d26654ba7b18bd3426a464049b58c9ea --- /dev/null +++ b/morph/grid/production/public-keys/deploy_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK50RwXncelNB4JAazoXEhCxXbJZ79qWcQMAWeX14H+W exarkun@baryon diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub new file mode 100644 index 0000000000000000000000000000000000000000..79248b8afc2e5d58ce0e2829c34266d377e2ffa5 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub @@ -0,0 +1 @@ +f4PF38t1ZRneFCV+12irDbMuG81WK6jiH0Ba+P+XtXM= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub new file mode 100644 index 0000000000000000000000000000000000000000..c085058430258c7c5a4c3fe6a2a2e87ebce56543 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub @@ -0,0 +1 @@ +yBdp154+SjyjTJM6ag1mbdnXORWrv/mJ01NJdkEe9VY= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub new file mode 100644 index 0000000000000000000000000000000000000000..5c6351937d9d746d6c1e0ebca3439dc49a1f4574 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub @@ -0,0 +1 @@ +G0//oetsCGa75x8rLsg98c9GT9a0ncf1yG9w2+5JV0M= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub new file mode 100644 index 0000000000000000000000000000000000000000..1ec8fbe3f88c3d126b1c7a19a3c80ff55cedbe0c --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub @@ -0,0 +1 @@ +Zq4OsMOTJ2NsVi00hB0x20mMqvoCrDUfleoI5rzIeEc= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub new file mode 100644 index 0000000000000000000000000000000000000000..a5ce0ad526a0a0b949488304c05f0cc055695634 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub @@ -0,0 +1 @@ +9ThSUgSNrykQEULj70QQyjlvtvGTmMPqsRMz8hc9xHA= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub new file mode 100644 index 0000000000000000000000000000000000000000..c54c728a732d7ca083f9f5ac9e1cb7d82475101f --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub @@ -0,0 +1 @@ +fPUnFOzBZRJDBdSR6iS5AaC40KKy/2REiM16hx+woxk= diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub new file mode 100644 index 0000000000000000000000000000000000000000..0ae6bb2adee18a318237aa020ab222be0b240aa9 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub @@ -0,0 +1 @@ +qS4rT+zjWrbXDhtEF4oyGv8/5oCIE1ZU9FF+O6AL8V4= diff --git a/morph/grid/production/public-keys/monitoringvpn/server.pub b/morph/grid/production/public-keys/monitoringvpn/server.pub new file mode 120000 index 0000000000000000000000000000000000000000..0e74cbd09e33c4771cfecb7efea12650c8bd3b51 --- /dev/null +++ b/morph/grid/production/public-keys/monitoringvpn/server.pub @@ -0,0 +1 @@ +172.23.23.1.pub \ No newline at end of file diff --git a/morph/grid/production/public-keys/users.nix b/morph/grid/production/public-keys/users.nix new file mode 100644 index 0000000000000000000000000000000000000000..8b586703740765b7a3d462e74ca3ef3cced68da7 --- /dev/null +++ b/morph/grid/production/public-keys/users.nix @@ -0,0 +1,2 @@ +let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGN4VQm3BIQKEFTw6aPrEwNuShf640N+Py2LOKznFCRT exarkun@bottom"; +in { "root" = key; "jcalderone" = key; } diff --git a/morph/grid/production/storage001-hardware.nix b/morph/grid/production/storage001-hardware.nix index 4cd9f59b76dd77b6e6e85709b3fbee771677b641..b2ca97c1db1b9721b93f2662d6e8d34189d5a0ab 100644 --- a/morph/grid/production/storage001-hardware.nix +++ b/morph/grid/production/storage001-hardware.nix @@ -12,6 +12,7 @@ boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; fileSystems."/" = { device = "/dev/disk/by-uuid/f72c1f46-6723-45bf-9ef7-92f31cc37589"; @@ -30,9 +31,12 @@ fsType = "zfs"; }; - swapDevices = - [ { device = "/dev/disk/by-uuid/f986a811-4912-4e9a-8bc3-01cb6926c4c6"; } - ]; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; + nix.maxJobs = lib.mkDefault 24; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; diff --git a/morph/grid/production/storage002-hardware.nix b/morph/grid/production/storage002-hardware.nix index 4fc3a4097e05ec8c38c86db6bfce92e2a1af6f35..2f354ad29930f048f7eb20b54a1504ed87db85a1 100644 --- a/morph/grid/production/storage002-hardware.nix +++ b/morph/grid/production/storage002-hardware.nix @@ -12,6 +12,7 @@ boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; fileSystems."/" = { device = "/dev/disk/by-uuid/0e92ada9-effb-42e2-a26a-9cdb529bcdc7"; @@ -30,9 +31,11 @@ fsType = "ext4"; }; - swapDevices = - [ { device = "/dev/disk/by-uuid/f762b5e2-bbdd-4a02-bbd9-0bf6b11e0ab5"; } - ]; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; nix.maxJobs = lib.mkDefault 24; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; diff --git a/morph/grid/production/storage003-hardware.nix b/morph/grid/production/storage003-hardware.nix index 9882f5372cecd52794e1500bdef30e367008496e..d8ffe5d59fb39ba4a9c6b1b73313f199a2ed980b 100644 --- a/morph/grid/production/storage003-hardware.nix +++ b/morph/grid/production/storage003-hardware.nix @@ -13,6 +13,7 @@ boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; boot.supportedFilesystems = [ "zfs" ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; fileSystems."/" = { device = "/dev/disk/by-uuid/240fc1f6-cd55-48a3-ac80-5b3550a32ef5"; @@ -31,7 +32,11 @@ fsType = "zfs"; }; - swapDevices = [ ]; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; nix.maxJobs = lib.mkDefault 24; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; diff --git a/morph/grid/production/storage004-hardware.nix b/morph/grid/production/storage004-hardware.nix index 07de74e20ef58ab474b02248bcb6eed6189e1079..1fe78a76e813605d8e181d5a858062f77114ba38 100644 --- a/morph/grid/production/storage004-hardware.nix +++ b/morph/grid/production/storage004-hardware.nix @@ -12,6 +12,7 @@ boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; fileSystems."/" = { device = "/dev/disk/by-uuid/d628122e-05d9-4212-b6a5-4b9516d85dbe"; @@ -25,7 +26,11 @@ fsType = "zfs"; }; - swapDevices = [ ]; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; nix.maxJobs = lib.mkDefault 32; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; diff --git a/morph/grid/production/storage005-hardware.nix b/morph/grid/production/storage005-hardware.nix index 9a5ad02725e30b00619978035772d60bec9fcb8a..e8f7b6391b4cb1c8d3e6059c1fd09512a0cc370b 100644 --- a/morph/grid/production/storage005-hardware.nix +++ b/morph/grid/production/storage005-hardware.nix @@ -12,6 +12,7 @@ boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; fileSystems."/" = { device = "/dev/disk/by-uuid/2653c6bb-396f-4911-b9ff-b68de8f9715d"; @@ -30,7 +31,11 @@ fsType = "zfs"; }; - swapDevices = [ ]; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; nix.maxJobs = lib.mkDefault 32; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; diff --git a/morph/grid/testing/.gitignore b/morph/grid/testing/.gitignore index db2fc0de62d01d6d7eec83f8f3e8c3b13b20392a..e3b6111c86090b06c38b9e5afd1fcd16838ddf47 100644 --- a/morph/grid/testing/.gitignore +++ b/morph/grid/testing/.gitignore @@ -1 +1 @@ -secrets +private-keys diff --git a/morph/grid/testing/config.json b/morph/grid/testing/config.json index a44b465f7f293f9d70c369a076c30b6cf810924f..a10840db52e8cd74bbac2a0ad38f4887c1a03258 100644 --- a/morph/grid/testing/config.json +++ b/morph/grid/testing/config.json @@ -1,7 +1,7 @@ -{ "publicStoragePort": 8898 -, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key" -, "stripeSecretKeyPath": "./secrets/stripe.secret" -, "monitoringvpnKeyDir": "./secrets/monitoringvpn" +{ "domain": "privatestorage-staging.com" +, "publicStoragePort": 8898 +, "privateKeyPath": "./private-keys" +, "publicKeyPath": "./public-keys" , "monitoringvpnEndpoint": "monitoring.privatestorage-staging.com:51820" , "passValue": 1000000 , "issuerDomains": [ @@ -14,4 +14,5 @@ , "https://privatestorage-staging.com" , "https://www.privatestorage-staging.com" ] +, "monitoringGoogleOAuthClientID": "802959152038-6esn1c6u2lm3j82lf29jvmn8s63hi8dc.apps.googleusercontent.com" } diff --git a/morph/grid/testing/grid.nix b/morph/grid/testing/grid.nix index e31a28f2eb7817f393f4e8b6b71972b7fd2f79f1..fbbbd9f13e49cfdc7fd2f0687fa2fe12df91ea33 100644 --- a/morph/grid/testing/grid.nix +++ b/morph/grid/testing/grid.nix @@ -1,54 +1,78 @@ -# Load the helper function and call it with arguments tailored for the testing -# grid. It will make the morph configuration for us. We share this function -# with the production grid and have one fewer possible point of divergence. -import ../../lib/make-grid.nix { - name = "Testing"; - config = ./config.json; - nodes = cfg: - let - sshUsers = import ./secrets/users.nix; - - # Get absolute vpn key directory path, as a string: - monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}"; - - # TBD: derive these automatically: - hostsMap = { - "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; - "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; - "172.23.23.12" = [ "storage001" "storage001.monitoringvpn" ]; +# See morph/grid/local/grid.nix for additional commentary. +let + pkgs = import <nixpkgs> { }; + + gridlib = import ../../lib; + rawConfig = pkgs.lib.trivial.importJSON ./config.json; + config = rawConfig // { + sshUsers = import ./public-keys/users.nix; + + # Convert relative paths to absolute so library code can resolve names + # correctly. + publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; + privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; + }; + + # Configure deployment management authorization for all systems in the grid. + deployment = { + services.private-storage.deployment = { + authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + gridName = "testing"; }; - vpnClientIPs = [ "172.23.23.11" "172.23.23.12" ]; - nodeExporterTargets = [ "monitoring" "payments" "storage001" ]; - - in { - "payments" = import ../../lib/make-issuer.nix (cfg // { - publicIPv4 = "18.194.183.13"; - monitoringvpnIPv4 = "172.23.23.11"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - hardware = ../../lib/issuer-aws.nix; - stateVersion = "19.03"; - }); - - "storage001" = import ../../lib/make-testing.nix (cfg // { - publicIPv4 = "3.120.26.190"; - monitoringvpnIPv4 = "172.23.23.12"; - inherit monitoringvpnKeyDir; - inherit sshUsers; - hardware = ./testing001-hardware.nix; - stateVersion = "19.03"; - }); - - "monitoring" = import ../../lib/make-monitoring.nix (cfg // { - publicIPv4 = "18.156.171.217"; - monitoringvpnIPv4 = "172.23.23.1"; - inherit monitoringvpnKeyDir; - inherit vpnClientIPs; - inherit hostsMap; - inherit nodeExporterTargets; - hardware = ../../lib/issuer-aws.nix; - stateVersion = "19.09"; - inherit sshUsers; - }); }; + + payments = { + imports = [ + gridlib.issuer + gridlib.hardware-aws + (gridlib.customize-issuer (config // { + monitoringvpnIPv4 = "172.23.23.11"; + })) + deployment + ]; + }; + + storage001 = { + imports = [ + gridlib.storage + gridlib.hardware-aws + ./testing001-hardware.nix + (gridlib.customize-storage (config // { + monitoringvpnIPv4 = "172.23.23.12"; + stateVersion = "19.03"; + })) + deployment + ]; + }; + + monitoring = { + imports = [ + gridlib.monitoring + gridlib.hardware-aws + (gridlib.customize-monitoring { + inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; + inherit (config) domain publicKeyPath privateKeyPath letsEncryptAdminEmail; + googleOAuthClientID = config.monitoringGoogleOAuthClientID; + monitoringvpnIPv4 = "172.23.23.1"; + stateVersion = "19.09"; + }) + deployment + ]; + }; + + # TBD: derive these automatically: + hostsMap = { + "172.23.23.1" = [ "monitoring" "monitoring.monitoringvpn" ]; + "172.23.23.11" = [ "payments" "payments.monitoringvpn" ]; + "172.23.23.12" = [ "storage001" "storage001.monitoringvpn" ]; + }; + vpnClientIPs = [ "172.23.23.11" "172.23.23.12" ]; + nodeExporterTargets = [ "monitoring" "payments" "storage001" ]; + paymentExporterTargets = [ "payments" ]; + +in { + network = { + description = "PrivateStorage.io Testing Grid"; + }; + inherit payments monitoring storage001; } diff --git a/morph/grid/testing/public-keys/deploy_key.pub b/morph/grid/testing/public-keys/deploy_key.pub new file mode 100644 index 0000000000000000000000000000000000000000..2dafd3cce2c83b8e4a32815c37c51ee890ba846c --- /dev/null +++ b/morph/grid/testing/public-keys/deploy_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB88qfLdoR5Pq9Us7vOVc6wBWmIDxme9MXYQSxxO+8/X exarkun@baryon diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub new file mode 100644 index 0000000000000000000000000000000000000000..94e7f1592034419c8a561531811bd6e63241271c --- /dev/null +++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub @@ -0,0 +1 @@ +iVS3L2DkH/pHAhiPpuduBMKlICPYmchHFfCg6n2ReUI= diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub new file mode 100644 index 0000000000000000000000000000000000000000..ed5b6822bd633df6b704fa0eda0e9250d4b198e2 --- /dev/null +++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub @@ -0,0 +1 @@ +sGUEH9+Mli1E1BFBMAHgPsnVlaD1EJKFaYOJ+dpyLy0= diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub new file mode 100644 index 0000000000000000000000000000000000000000..0c79d3a917db9f5caed071eabeae9d4974d660db --- /dev/null +++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub @@ -0,0 +1 @@ +wvpkXigLG2zvmLhxsV2cmN/IgF+nLednV6uENvI6fh0= diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub new file mode 100644 index 0000000000000000000000000000000000000000..31fd40caf83b95fd8e4566af21b9d6e59a70e629 --- /dev/null +++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub @@ -0,0 +1 @@ +5t9t6DOcYMQJNtnsG5/Ek+OmSX1mZgbMAHSWlJQKuxc= diff --git a/morph/grid/testing/public-keys/monitoringvpn/server.pub b/morph/grid/testing/public-keys/monitoringvpn/server.pub new file mode 120000 index 0000000000000000000000000000000000000000..0e74cbd09e33c4771cfecb7efea12650c8bd3b51 --- /dev/null +++ b/morph/grid/testing/public-keys/monitoringvpn/server.pub @@ -0,0 +1 @@ +172.23.23.1.pub \ No newline at end of file diff --git a/morph/grid/testing/public-keys/users.nix b/morph/grid/testing/public-keys/users.nix new file mode 100644 index 0000000000000000000000000000000000000000..d6a965011065cfe39713adfb797c190eb8dd1ecd --- /dev/null +++ b/morph/grid/testing/public-keys/users.nix @@ -0,0 +1,9 @@ +let + jcalderone = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN4GenAY/YLGuf1WoMXyyVa3S9i4JLQ0AG+pt7nvcLlQ exarkun@baryon"; + flo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHx7wJQNqKn8jOC4AxySRL2UxidNp7uIK9ad3pMb1ifF flo@fs-la"; +in + { + "root" = jcalderone; + inherit jcalderone; + inherit flo; + } diff --git a/morph/grid/testing/testing001-hardware.nix b/morph/grid/testing/testing001-hardware.nix index 958a247862a7e4bb2581e7d1bb85cc0f85f3ea24..5dceb16af1deaeb4668e67cbb65715ae79aa55d9 100644 --- a/morph/grid/testing/testing001-hardware.nix +++ b/morph/grid/testing/testing001-hardware.nix @@ -1,12 +1,9 @@ { - imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ]; - ec2.hvm = true; - boot.supportedFilesystems = [ "zfs" ]; networking.hostId = "10000000"; # Manually created using: - # zpool create -m legacy -o ashift=12 root raidz /dev/disk/by-id/{nvme-nvme.1d0f-766f6c3038623133353836383465643436363430-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3034653531383066303134633436653034-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3062333164633831386366623231373730-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3061353939623438336661353933636664-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001} + # zpool create -m legacy -o ashift=12 root /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol047fc3ea6bafdcc58 fileSystems."/storage" = { device = "root"; fsType = "zfs"; diff --git a/morph/lib/customize-issuer.nix b/morph/lib/customize-issuer.nix new file mode 100644 index 0000000000000000000000000000000000000000..1c0d668fbd4ae59bab115c2116b7fa377395dcfc --- /dev/null +++ b/morph/lib/customize-issuer.nix @@ -0,0 +1,94 @@ +# Define a function which returns a value which fills in all the holes left by +# ``issuer.nix``. +{ + # A path on the deployment system of a directory containing all of the + # public keys for the system. For example, this holds Wireguard public keys + # for the VPN configuration and SSH public keys to configure SSH + # authentication. + publicKeyPath + + # A path on the deployment system of a directory containing all of the + # corresponding private keys for the system. +, privateKeyPath + + # A string giving the IP address and port number (":"-separated) of the VPN + # server. +, monitoringvpnEndpoint + + # A string giving the VPN IPv4 address for this system. +, monitoringvpnIPv4 + + # A string giving the domain name associated with this grid. This is meant + # to be combined with the hostname for this system to produce a + # fully-qualified domain name. For example, an issuer might have "payments" + # as its hostname and belong to a grid with the domain + # "example-grid.invalid". This ``domain`` parameter should have the value + # ``"example-grid.invalid"`` for the system figure out that + # ``payments.example-grid.invalid`` is the name of this system. +, domain + + # A set mapping usernames as strings to SSH public keys as strings. For + # each element of the site, the indicated user is configured on the system + # with the indicated SSH key as an authorized key. +, sshUsers + + # A string giving an email address to use for Let's Encrypt registration and + # certificate issuance. +, letsEncryptAdminEmail + + # A list of strings giving the domain names that point at this issuer + # system. These will all be included in Let's Encrypt certificate. +, issuerDomains + + # A list of strings giving CORS Origins will the issuer will be configured + # to allow. +, allowedChargeOrigins +, ... +}: +{ config, ... }: { + # The morph default deployment target the name of the node in the network + # attrset. We don't always want to give the node its proper public address + # there (because it depends on which domain is associated with the grid + # being configured and using variable names complicates a lot of things). + # Instead, just tell morph how to reach the node here - by using its fully + # qualified domain name. + deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; + + deployment.secrets = { + # A path on the deployment system to a file containing the Ristretto + # signing key. This is used as the source of the Ristretto signing key + # morph secret. + "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key"; + + # A path on the deployment system to a file containing the Stripe secret + # key. This is used as the source of the Stripe secret key morph secret. + "stripe-secret-key".source = "${privateKeyPath}/stripe.secret"; + + # ``.../monitoringvpn`` is a path on the deployment system of a directory + # containing a number of VPN-related secrets. This is expected to contain + # a number of files named like ``<VPN IPv4 address>.key`` containing the + # VPN private key for the corresponding host. It must also contain + # ``server.pub`` and ``preshared.key`` holding the VPN server's public key + # and the pre-shared key, respectively. All of these things are used as + # the sources of various VPN-related morph secrets. + "monitoringvpn-secret-key".source = "${privateKeyPath}/monitoringvpn/${monitoringvpnIPv4}.key"; + "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key"; + }; + + networking.domain = domain; + + services.private-storage.sshUsers = sshUsers; + services.private-storage.monitoring.vpn.client = { + enable = true; + ip = monitoringvpnIPv4; + endpoint = monitoringvpnEndpoint; + endpointPublicKeyFile = "${publicKeyPath}/monitoringvpn/server.pub"; + }; + + services.private-storage-issuer = { + inherit letsEncryptAdminEmail allowedChargeOrigins; + domains = issuerDomains; + }; + + system.stateVersion = "19.03"; +} diff --git a/morph/lib/customize-monitoring.nix b/morph/lib/customize-monitoring.nix new file mode 100644 index 0000000000000000000000000000000000000000..f5b820a272fcfd4ea7106af32ad2fd0ac5c8ece3 --- /dev/null +++ b/morph/lib/customize-monitoring.nix @@ -0,0 +1,107 @@ +# Define a function which returns a value which fills in all the holes left by +# ``monitoring.nix``. +{ + # A set mapping VPN IP addresses as strings to lists of hostnames as + # strings. The system's ``/etc/hosts`` will be populated with this + # information. Apart from helping with normal forward resolution, this + # *also* gives us reverse resolution from the VPN IPs to hostnames which + # allows Grafana to show us hostnames instead of VPN IP addresses. + hostsMap + + # See ``customize-issuer.nix``. +, publicKeyPath +, privateKeyPath +, monitoringvpnIPv4 +, domain +, letsEncryptAdminEmail + + # A list of VPN IP addresses as strings indicating which clients will be + # allowed onto the VPN. +, vpnClientIPs + + # A list of VPN clients (IP addresses or hostnames) as strings indicating + # which nodes to scrape "nodeExporter" metrics from. +, nodeExporterTargets + + # A list of VPN clients (IP addresses or hostnames) as strings indicating + # which nodes to scrape "nginxExporter" metrics from. +, nginxExporterTargets ? [] + + # A list of VPN clients (IP addresses or hostnames) as strings indicating + # which nodes to scrape PaymentServer metrics from. +, paymentExporterTargets ? [] + + # A string containing the GSuite OAuth2 ClientID to use to authenticate + # logins to Grafana. +, googleOAuthClientID + + # A string giving the NixOS state version for the system. +, stateVersion +, ... +}: +{ config, ... }: { + # See customize-issuer.nix for an explanatoin of targetHost value. + deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; + + deployment.secrets = let + # When Grafana SSO is disabled there is not necessarily any client secret + # available. Avoid telling morph that there is one in this case (so it + # avoids trying to read it and then failing). Even if the secret did + # exist, if SSO is disabled there's no point sending the secret to the + # server. + # + # Also, we have to define this whole secret here so that we can configure + # it completely or not at all. morph gets angry if we half configure it + # (say, by just omitting the "source" value). + grafanaSSO = + if googleOAuthClientID == "" + then { } + else { + "grafana-google-sso-secret" = { + source = "${privateKeyPath}/grafana-google-sso.secret"; + destination = "/run/keys/grafana-google-sso.secret"; + owner.user = config.systemd.services.grafana.serviceConfig.User; + owner.group = config.users.users.grafana.group; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "grafana.service"]; + }; + "grafana-admin-password" = { + source = "${privateKeyPath}/grafana-admin.password"; + destination = "/run/keys/grafana-admin.password"; + owner.user = config.systemd.services.grafana.serviceConfig.User; + owner.group = config.users.users.grafana.group; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "grafana.service"]; + }; + }; + monitoringvpn = { + "monitoringvpn-private-key".source = "${privateKeyPath}/monitoringvpn/server.key"; + "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key"; + }; + in + grafanaSSO // monitoringvpn; + + networking.domain = domain; + networking.hosts = hostsMap; + + services.private-storage.monitoring.vpn.server = { + enable = true; + ip = monitoringvpnIPv4; + inherit vpnClientIPs; + pubKeysPath = "${publicKeyPath}/monitoringvpn"; + }; + + services.private-storage.monitoring.prometheus = { + inherit nodeExporterTargets; + inherit nginxExporterTargets; + inherit paymentExporterTargets; + }; + + services.private-storage.monitoring.grafana = { + inherit letsEncryptAdminEmail; + inherit googleOAuthClientID; + domain = "${config.networking.hostName}.${config.networking.domain}"; + }; + + system.stateVersion = stateVersion; +} diff --git a/morph/lib/customize-storage.nix b/morph/lib/customize-storage.nix new file mode 100644 index 0000000000000000000000000000000000000000..68655874efd9ba39b52dacfdddaedb54863ed769 --- /dev/null +++ b/morph/lib/customize-storage.nix @@ -0,0 +1,47 @@ +# Define a function which returns a value which fills in all the holes left by +# ``storage.nix``. +{ + # See ``customize-issuer.nix`` + privateKeyPath +, publicKeyPath +, monitoringvpnEndpoint +, monitoringvpnIPv4 +, sshUsers +, domain + + # An integer giving the value of a single pass in byte×months. +, passValue + + # An integer giving the port number to include in Tahoe storage service + # advertisements and on which to listen for storage connections. +, publicStoragePort + + # A string giving the NixOS state version for the system. +, stateVersion +, ... +}: +{ config, ... }: { + # See customize-issuer.nix for an explanatoin of targetHost value. + deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; + + deployment.secrets = { + "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key"; + "monitoringvpn-secret-key".source = "${privateKeyPath}/monitoringvpn/${monitoringvpnIPv4}.key"; + "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key"; + }; + + networking.domain = domain; + + services.private-storage = { + inherit sshUsers passValue publicStoragePort; + }; + + services.private-storage.monitoring.vpn.client = { + enable = true; + ip = monitoringvpnIPv4; + endpoint = monitoringvpnEndpoint; + endpointPublicKeyFile = "${publicKeyPath}/monitoringvpn/server.pub"; + }; + + system.stateVersion = stateVersion; +} diff --git a/morph/lib/default.nix b/morph/lib/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..bdd92f4bfe52eba2e19df3ac73a087a4af4a53dc --- /dev/null +++ b/morph/lib/default.nix @@ -0,0 +1,16 @@ +# Gather up the grid library functionality and present it in a (somewhat) +# coherent public interface. Application code should prefer these names over +# directly importing the source files in this directory. +{ + hardware-aws = import ./issuer-aws.nix; + hardware-virtual = import ./hardware-virtual.nix; + + issuer = import ./issuer.nix; + customize-issuer = import ./customize-issuer.nix; + + storage = import ./storage.nix; + customize-storage = import ./customize-storage.nix; + + monitoring = import ./monitoring.nix; + customize-monitoring = import ./customize-monitoring.nix; +} diff --git a/morph/grid/local/virtual-hardware.nix b/morph/lib/hardware-virtual.nix similarity index 95% rename from morph/grid/local/virtual-hardware.nix rename to morph/lib/hardware-virtual.nix index d5e9067bd5f3b3ca2ea1bb46746253fa39b25cf6..cf1582792bff77c491210ee5e91f99bfbffbf9f3 100644 --- a/morph/grid/local/virtual-hardware.nix +++ b/morph/lib/hardware-virtual.nix @@ -11,6 +11,7 @@ boot.initrd.availableKernelModules = [ "ata_piix" "sd_mod" "sr_mod" ]; boot.initrd.kernelModules = [ ]; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; boot.kernelModules = [ ]; boot.extraModulePackages = [ ]; @@ -33,4 +34,3 @@ # We want to push packages with morph without having to sign them nix.trustedUsers = [ "@wheel" "root" "vagrant" ]; } - diff --git a/morph/lib/issuer-aws.nix b/morph/lib/issuer-aws.nix index b4d4757ad5597b69363ef12e4297aec80913f00e..a66ab72addd43da1feb96bdd86d46312ec327fd3 100644 --- a/morph/lib/issuer-aws.nix +++ b/morph/lib/issuer-aws.nix @@ -1,4 +1,20 @@ -{ +{ name, lib, ... }: { imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ]; + + # amazon-image.nix isn't quite aware of nvme-attached storage so give it a + # little help configuring grub. + boot.loader.grub.device = lib.mkForce "/dev/nvme0n1"; + ec2.hvm = true; + boot.kernel.sysctl = { "vm.swappiness" = 0; }; + swapDevices = [ { + device = "/var/swapfile"; + size = 8192; # megabytes + randomEncryption = true; + } ]; + + # Break the tie between AWS and morph for the hostname by forcing the + # morph-supplied name. See also + # <https://github.com/DBCDK/morph/issues/146>. + networking.hostName = name; } diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix new file mode 100644 index 0000000000000000000000000000000000000000..51046b436e297cdc5034134e3503556e8030588c --- /dev/null +++ b/morph/lib/issuer.nix @@ -0,0 +1,59 @@ +# This is all of the static NixOS system configuration necessary to specify an +# "issuer"-type system. The configuration has various holes in it which must +# be filled somehow. These holes correspond to configuration which is not +# statically known. This value is suitable for use as a module to be imported +# into a more complete system configuration. It is expected that the holes +# will be filled by a sibling module created by ``customize-issuer.nix``. +rec { + deployment = { + secrets = { + "ristretto-signing-key" = { + destination = "/run/keys/ristretto.signing-key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; + }; + "stripe-secret-key" = { + destination = "/run/keys/stripe.secret-key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; + }; + + "monitoringvpn-secret-key" = { + destination = "/run/keys/monitoringvpn/client.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + "monitoringvpn-preshared-key" = { + destination = "/run/keys/monitoringvpn/preshared.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + }; + }; + + imports = [ + # Allow us to remotely trigger updates to this system. + ../../nixos/modules/deployment.nix + + ../../nixos/modules/issuer.nix + ../../nixos/modules/monitoring/vpn/client.nix + ../../nixos/modules/monitoring/exporters/node.nix + ]; + + services.private-storage-issuer = { + enable = true; + tls = true; + ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; + stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination; + database = "SQLite3"; + databasePath = "/var/db/vouchers.sqlite3"; + }; +} diff --git a/morph/lib/make-grid.nix b/morph/lib/make-grid.nix deleted file mode 100644 index de10df1e9a62ee0ac7fde98070743ee4a9cf484b..0000000000000000000000000000000000000000 --- a/morph/lib/make-grid.nix +++ /dev/null @@ -1,19 +0,0 @@ -# Define a function for making a morph configuration for a storage grid. It -# takes two arguments. A string like "Production" giving the name of the grid -# and a function that takes the grid configuration as an argument and returns -# a set of nodes specifying the addresses and NixOS configurations for each -# server in the morph network. -{ name, config, nodes }: -let - pkgs = import <nixpkgs> { }; - # Load our JSON configuration for later use. - cfg = pkgs.lib.trivial.importJSON config; -in -{ - network = { - # Make all of the hosts in this network use the nixpkgs we pinned above. - inherit pkgs; - # This is just for human consumption as far as I can tell. - description = "PrivateStorage.io ${name} Grid"; - }; -} // (nodes cfg) diff --git a/morph/lib/make-issuer.nix b/morph/lib/make-issuer.nix deleted file mode 100644 index bbdf0cebbf770738e9ccb997daec75e58df021b5..0000000000000000000000000000000000000000 --- a/morph/lib/make-issuer.nix +++ /dev/null @@ -1,91 +0,0 @@ -{ hardware -, ristrettoSigningKeyPath -, stripeSecretKeyPath -, issuerDomains -, letsEncryptAdminEmail -, allowedChargeOrigins -, sshUsers -, stateVersion -, publicIPv4 -, monitoringvpnKeyDir ? null -, monitoringvpnIPv4 ? null -, monitoringvpnEndpoint ? null -, ... -}: let - - enableVpn = monitoringvpnKeyDir != null && - monitoringvpnIPv4 != null && - monitoringvpnEndpoint != null; - - vpnSecrets = if !enableVpn then {} else { - "monitoringvpn-secret-key" = { - source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key"; - destination = "/run/keys/monitoringvpn/client.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - "monitoringvpn-preshared-key" = { - source = monitoringvpnKeyDir + "/preshared.key"; - destination = "/run/keys/monitoringvpn/preshared.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - }; - -in rec { - deployment = { - targetHost = publicIPv4; - - secrets = { - "ristretto-signing-key" = { - source = ristrettoSigningKeyPath; - destination = "/run/keys/ristretto.signing-key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; - }; - "stripe-secret-key" = { - source = stripeSecretKeyPath; - destination = "/run/keys/stripe.secret-key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; - }; - } // vpnSecrets; - }; - - imports = [ - hardware - ../../nixos/modules/issuer.nix - ../../nixos/modules/monitoring/vpn/client.nix - ../../nixos/modules/monitoring/exporters/node.nix - ]; - - services.private-storage.sshUsers = sshUsers; - services.private-storage-issuer = { - enable = true; - tls = true; - ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; - stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination; - database = "SQLite3"; - databasePath = "/var/db/vouchers.sqlite3"; - inherit letsEncryptAdminEmail; - domains = issuerDomains; - inherit allowedChargeOrigins; - }; - - system.stateVersion = stateVersion; - - services.private-storage.monitoring.vpn.client = if !enableVpn then {} else { - enable = true; - ip = monitoringvpnIPv4; - endpoint = monitoringvpnEndpoint; - endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub"; - }; -} diff --git a/morph/lib/make-monitoring.nix b/morph/lib/make-monitoring.nix deleted file mode 100644 index 592a859657e624e8fdf5632f8144c5acc6919e8c..0000000000000000000000000000000000000000 --- a/morph/lib/make-monitoring.nix +++ /dev/null @@ -1,77 +0,0 @@ -{ publicIPv4 -, hardware -, publicStoragePort -, ristrettoSigningKeyPath -, passValue -, sshUsers -, stateVersion -, monitoringvpnIPv4 ? null -, monitoringvpnKeyDir ? null -, vpnClientIPs ? null -, nodeExporterTargets ? [] -, nginxExporterTargets ? [] -, hostsMap ? {} -, ... }: let - - enableVpn = monitoringvpnKeyDir != null && - monitoringvpnIPv4 != null && - vpnClientIPs != null; - - vpnSecrets = if !enableVpn then {} else { - "monitoringvpn-private-key" = { - source = monitoringvpnKeyDir + "/server.key"; - destination = "/run/keys/monitoringvpn/server.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - "monitoringvpn-preshared-key" = { - source = monitoringvpnKeyDir + "/preshared.key"; - destination = "/run/keys/monitoringvpn/preshared.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - }; - -in rec { - - deployment = { - targetHost = publicIPv4; - secrets = vpnSecrets; - }; - - imports = [ - hardware - ../../nixos/modules/monitoring/vpn/server.nix - ../../nixos/modules/monitoring/server/grafana.nix - ../../nixos/modules/monitoring/server/prometheus.nix - ../../nixos/modules/monitoring/exporters/node.nix - # Loki 0.3.0 from Nixpkgs 19.09 is too old and does not work: - # ../../nixos/modules/monitoring/server/loki.nix - ]; - - services.private-storage.monitoring.vpn.server = if !enableVpn then {} else { - enable = true; - ip = monitoringvpnIPv4; - inherit vpnClientIPs; - pubKeysPath = monitoringvpnKeyDir; - }; - - services.private-storage.monitoring.grafana = { - domain = "monitoring.private.storage"; - prometheusUrl = "http://localhost:9090/"; - lokiUrl = "http://localhost:3100/"; - }; - - services.private-storage.monitoring.prometheus = { - inherit nodeExporterTargets; - inherit nginxExporterTargets; - }; - - system.stateVersion = stateVersion; - - networking.hosts = hostsMap; -} diff --git a/morph/lib/make-storage.nix b/morph/lib/make-storage.nix deleted file mode 100644 index 6619336d758f69a677e9178592357480aed3f0c8..0000000000000000000000000000000000000000 --- a/morph/lib/make-storage.nix +++ /dev/null @@ -1,109 +0,0 @@ -# Define the function that defines the node. -{ cfg # Get the configuration that's specific to this node. -, hardware # The path to the hardware configuration for this node. -, publicStoragePort # The storage port number on which to accept connections. -, ristrettoSigningKeyPath # The *local* path to the Ristretto signing key file. -, passValue # Bytes component of size×time value of passes. -, sshUsers # Users for which to configure SSH access to this node. -, stateVersion # The value for system.stateVersion on this node. - # This value determines the NixOS release with - # which your system is to be compatible, in order - # to avoid breaking some software such as - # database servers. You should change this only - # after NixOS release notes say you should. -, monitoringvpnKeyDir ? null # The directory that holds the VPN keys. -, monitoringvpnIPv4 ? null # This node's IP in the monitoring VPN. -, monitoringvpnEndpoint ? null # The VPN server and port. -, ... -}: let - - enableVpn = monitoringvpnKeyDir != null && - monitoringvpnIPv4 != null && - monitoringvpnEndpoint != null; - - vpnSecrets = if !enableVpn then {} else { - "monitoringvpn-secret-key" = { - source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key"; - destination = "/run/keys/monitoringvpn/client.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - "monitoringvpn-preshared-key" = { - source = monitoringvpnKeyDir + "/preshared.key"; - destination = "/run/keys/monitoringvpn/preshared.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - }; - -in rec { - deployment = { - targetHost = cfg.publicIPv4; - - secrets = { - "ristretto-signing-key" = { - source = ristrettoSigningKeyPath; - destination = "/run/keys/ristretto.signing-key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - # Service name here matches the name defined by our tahoe-lafs nixos - # module. It would be nice to not have to hard-code it here. Can we - # extract it from the tahoe-lafs nixos module somehow? - action = ["sudo" "systemctl" "restart" "tahoe.storage.service"]; - }; - } // vpnSecrets; - }; - - # Any extra NixOS modules to load on this server. - imports = [ - # Include the results of the hardware scan. - hardware - # Configure it as a system operated by 100TB. - ../../nixos/modules/100tb.nix - # Bring in our module for configuring the Tahoe-LAFS service and other - # Private Storage-specific things. - ../../nixos/modules/private-storage.nix - # Connect to the monitoringvpn. - ../../nixos/modules/monitoring/vpn/client.nix - # Expose base system metrics over the monitoringvpn. - ../../nixos/modules/monitoring/exporters/node.nix - ]; - - # Pass the configuration specific to this host to the 100TB module to be - # expanded into a complete system configuration. See the 100tb module for - # handling of this value. - # - # The module name is quoted because `1` makes `100tb` look an awful lot like - # it should be a number. - "100tb".config = cfg; - - # Turn on the Private Storage (Tahoe-LAFS) service. - services.private-storage = { - # Yep. Turn it on. - enable = true; - # Get the public IPv4 address from the node configuration. - inherit (cfg) publicIPv4; - # And the port to operate on is specified via parameter. - inherit publicStoragePort; - # Give it the Ristretto signing key, too, to support authorization. - ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; - # Assign the configured pass value. - inherit passValue; - # It gets the users, too. - inherit sshUsers; - }; - - system.stateVersion = stateVersion; - - services.private-storage.monitoring.vpn.client = if !enableVpn then {} else { - enable = true; - ip = monitoringvpnIPv4; - endpoint = monitoringvpnEndpoint; - endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub"; - }; -} diff --git a/morph/lib/make-testing.nix b/morph/lib/make-testing.nix deleted file mode 100644 index 3f6e767db5ee734a8ca2314b216d4fa602c01907..0000000000000000000000000000000000000000 --- a/morph/lib/make-testing.nix +++ /dev/null @@ -1,80 +0,0 @@ -{ publicIPv4 -, hardware -, publicStoragePort -, ristrettoSigningKeyPath -, passValue -, sshUsers -, stateVersion -, monitoringvpnKeyDir ? null -, monitoringvpnIPv4 ? null -, monitoringvpnEndpoint ? null -, ... }: let - - enableVpn = monitoringvpnKeyDir != null && - monitoringvpnIPv4 != null && - monitoringvpnEndpoint != null; - - vpnSecrets = if !enableVpn then {} else { - "monitoringvpn-secret-key" = { - source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key"; - destination = "/run/keys/monitoringvpn/client.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - "monitoringvpn-preshared-key" = { - source = monitoringvpnKeyDir + "/preshared.key"; - destination = "/run/keys/monitoringvpn/preshared.key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; - }; - }; - -in rec { - - deployment = { - targetHost = publicIPv4; - - secrets = { - "ristretto-signing-key" = { - source = ristrettoSigningKeyPath; - destination = "/run/keys/ristretto.signing-key"; - owner.user = "root"; - owner.group = "root"; - permissions = "0400"; - # Service name here matches the name defined by our tahoe-lafs nixos - # module. It would be nice to not have to hard-code it here. Can we - # extract it from the tahoe-lafs nixos module somehow? - action = ["sudo" "systemctl" "restart" "tahoe.storage.service"]; - }; - } // vpnSecrets; - }; - - imports = [ - hardware - ../../nixos/modules/private-storage.nix - ../../nixos/modules/monitoring/vpn/client.nix - ../../nixos/modules/monitoring/exporters/node.nix - ]; - - services.private-storage = - { enable = true; - inherit publicIPv4; - inherit publicStoragePort; - ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; - inherit passValue; - inherit sshUsers; - }; - - system.stateVersion = stateVersion; - - services.private-storage.monitoring.vpn.client = if !enableVpn then {} else { - enable = true; - ip = monitoringvpnIPv4; - endpoint = monitoringvpnEndpoint; - endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub"; - }; -} diff --git a/morph/lib/monitoring.nix b/morph/lib/monitoring.nix new file mode 100644 index 0000000000000000000000000000000000000000..d8af93b24119ba6dff5ce63a5b2d16fbd18edb71 --- /dev/null +++ b/morph/lib/monitoring.nix @@ -0,0 +1,34 @@ +# Similar to ``issuer.nix`` but for a "monitoring"-type system. Holes are +# filled by ``customize-monitoring.nix``. +rec { + deployment = { + secrets = { + "monitoringvpn-private-key" = { + destination = "/run/keys/monitoringvpn/server.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + "monitoringvpn-preshared-key" = { + destination = "/run/keys/monitoringvpn/preshared.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + }; + }; + + imports = [ + # Allow us to remotely trigger updates to this system. + ../../nixos/modules/deployment.nix + + ../../nixos/modules/monitoring/vpn/server.nix + ../../nixos/modules/monitoring/server/grafana.nix + ../../nixos/modules/monitoring/server/prometheus.nix + ../../nixos/modules/monitoring/exporters/node.nix + # Loki 0.3.0 from Nixpkgs 19.09 is too old and does not work: + # ../../nixos/modules/monitoring/server/loki.nix + ]; +} diff --git a/morph/lib/storage.nix b/morph/lib/storage.nix new file mode 100644 index 0000000000000000000000000000000000000000..ebad3d17e17e0098f6e098d61d7c614fde91b31e --- /dev/null +++ b/morph/lib/storage.nix @@ -0,0 +1,53 @@ +# Similar to ``issuer.nix`` but for a "storage"-type system. Holes are filled +# by ``customize-storage.nix``. +rec { + deployment = { + secrets = { + "ristretto-signing-key" = { + destination = "/run/keys/ristretto.signing-key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + # Service name here matches the name defined by our tahoe-lafs nixos + # module. It would be nice to not have to hard-code it here. Can we + # extract it from the tahoe-lafs nixos module somehow? + action = ["sudo" "systemctl" "restart" "tahoe.storage.service"]; + }; + "monitoringvpn-secret-key" = { + destination = "/run/keys/monitoringvpn/client.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + "monitoringvpn-preshared-key" = { + destination = "/run/keys/monitoringvpn/preshared.key"; + owner.user = "root"; + owner.group = "root"; + permissions = "0400"; + action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"]; + }; + }; + }; + + # Any extra NixOS modules to load on this server. + imports = [ + # Allow us to remotely trigger updates to this system. + ../../nixos/modules/deployment.nix + # Bring in our module for configuring the Tahoe-LAFS service and other + # Private Storage-specific things. + ../../nixos/modules/private-storage.nix + # Connect to the monitoringvpn. + ../../nixos/modules/monitoring/vpn/client.nix + # Expose base system metrics over the monitoringvpn. + ../../nixos/modules/monitoring/exporters/node.nix + ]; + + # Turn on the Private Storage (Tahoe-LAFS) service. + services.private-storage = { + # Yep. Turn it on. + enable = true; + # Give it the Ristretto signing key to support authorization. + ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; + }; +} diff --git a/morph/grid/local/vagrant-guest.nix b/morph/lib/vagrant-guest.nix similarity index 92% rename from morph/grid/local/vagrant-guest.nix rename to morph/lib/vagrant-guest.nix index 9e8e6d8ccab25d98d11738ff7df4a574c5cfd724..360671f5e8391571d37da6db37b2de8dc02b66bd 100644 --- a/morph/grid/local/vagrant-guest.nix +++ b/morph/lib/vagrant-guest.nix @@ -1,6 +1,6 @@ # Minimal configuration that vagrant depends on -{ config, pkgs, ... }: +{ config, pkgs, lib, ... }: let # Vagrant uses an insecure shared private key by default, but we # don't use the authorizedKeys attribute under users because it should be @@ -22,8 +22,10 @@ in # Enable the OpenSSH daemon. services.openssh.enable = true; - # Wireguard kernel module - boot.extraModulePackages = [ config.boot.kernelPackages.wireguard ]; + # Wireguard kernel module for Kernels < 5.6 + boot = lib.mkIf (lib.versionOlder pkgs.linuxPackages.kernel.version "5.6") { + extraModulePackages = [ config.boot.kernelPackages.wireguard ] ; + }; # Enable DBus services.dbus.enable = true; diff --git a/nixos/modules/deployment.nix b/nixos/modules/deployment.nix new file mode 100755 index 0000000000000000000000000000000000000000..b0a5e3c4c761d188922a076643fcd3a25a4b81f0 --- /dev/null +++ b/nixos/modules/deployment.nix @@ -0,0 +1,118 @@ +# A NixOS module which enables remotely-triggered deployment updates. +{ config, lib, pkgs, ... }: +let + # A handy alias for our part of the configuration. + cfg = config.services.private-storage.deployment; + + # Compute an authorized_keys line that allows the holder of a certain key to + # execute a certain command *only*. + restrictedKey = + { authorizedKey, command, gridName }: + # `restrict` means "disable all the things" then `command` means "but + # enable running this one command" (the client does not have to supply the + # command; if they authenticate, this is the command that will run). + "restrict,command=\"${command} ${gridName}\" ${authorizedKey}"; +in { + options = { + services.private-storage.deployment.authorizedKey = lib.mkOption { + type = lib.types.str; + example = lib.literalExample '' + ssh-ed25519 AAAAC3N... + ''; + description = '' + The SSH public key to authorize to trigger a deployment update. + ''; + }; + services.private-storage.deployment.gridName = lib.mkOption { + type = lib.types.str; + example = lib.literalExample "staging"; + description = '' + The name of the grid configuration to use to update this deployment. + ''; + }; + }; + + config = { + # Configure the system to use our binary cache so that deployment updates + # only require downloading pre-built software, not building it ourselves. + nix = { + binaryCachePublicKeys = [ + "saxtons.private.storage:MplOcEH8G/6mRlhlKkbA8GdeFR3dhCFsSszrspE/ZwY=" + ]; + binaryCaches = [ + "http://saxtons.private.storage" + ]; + }; + + # Create a one-time service that will set up an ssh key that allows the + # deployment user to authorize as root to perform the system update with + # `morph deploy`. + systemd.services.authorize-morph-as-root = { + enable = true; + serviceConfig = { + # Tell systemd that the service is a process that runs and then exits. + # By being "oneshot" instead of "simple" any dependencies are not + # started until after the process exits. We have no dependencies yet + # but if we did it would be more correct for them to wait until we are + # done. + # + # It is not clear that "oneshot" means "run once" though (maybe it + # does, I can't tell) so the script is robust in the face of repeated + # runs even though it should only ever need to be run once. + Type = "oneshot"; + }; + wantedBy = [ + # Run this to reach the multi-user target, a good target that is + # reached in the typical course of system startup. + "multi-user.target" + ]; + # Here's the program to run for this unit. It's a shell script that + # creates an ssh key that authorized root access via ssh and give it to + # the deployment user. If such a key appears to exist already, do + # nothing. + script = '' + KEY=~deployment/.ssh/morph_key + TMP="$KEY"_tmp + if [ ! -e "$KEY" ]; then + mkdir -p ~deployment/.ssh ~root/.ssh + chown deployment ~deployment/.ssh + ${pkgs.openssh}/bin/ssh-keygen -f "$TMP" + cat "$TMP".pub >> ~root/.ssh/authorized_keys + mv "$TMP".pub "$KEY".pub + mv "$TMP" "$KEY" + chown deployment "$KEY" + fi + ''; + }; + + # Raise the hard-limit on the size of $XDG_RUNTIME_DIR (ie + # /run/user/<uid>). The default of 10% is too small on some systems for + # the temporary state morph creates to do the self-update. + services.logind.extraConfig = '' + RuntimeDirectorySize=50% + ''; + + # Configure the deployment user. + users.users.deployment = { + # A user must be either normal or system. A normal user uses the + # default shell, has a home directory created for it at the usual + # location, and is in the "users" group. That's pretty much what we + # want for the deployment user. + isNormalUser = true; + + packages = [ + # update-deployment dependencies + pkgs.morph + pkgs.git + ]; + + # Authorize the supplied key to run the deployment update command. + openssh.authorizedKeys.keys = [ + (restrictedKey { + inherit (cfg) authorizedKey gridName; + command = ./update-deployment; + }) + ]; + }; + }; +} diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix index fb93ce35cce8c9cadbad5a04e888b0cca991f9c7..da56a43012b7e53a6d5ced17123eb3d898b24f3e 100644 --- a/nixos/modules/issuer.nix +++ b/nixos/modules/issuer.nix @@ -2,6 +2,8 @@ # ZKAPs. { lib, pkgs, config, ... }: let cfg = config.services.private-storage-issuer; + # Our own nixpkgs fork: + ourpkgs = import ../../nixpkgs-ps.nix {}; in { imports = [ # Give it a good SSH configuration. @@ -11,7 +13,7 @@ in { options = { services.private-storage-issuer.enable = lib.mkEnableOption "PrivateStorage ZKAP Issuer Service"; services.private-storage-issuer.package = lib.mkOption { - default = pkgs.zkapissuer.components.exes."PaymentServer-exe"; + default = ourpkgs.zkapissuer.components.exes."PaymentServer-exe"; type = lib.types.package; example = lib.literalExample "pkgs.zkapissuer.components.exes.\"PaymentServer-exe\""; description = '' diff --git a/nixos/modules/monitoring/server/grafana-config/services-overview.json b/nixos/modules/monitoring/server/grafana-config/services-overview.json new file mode 100644 index 0000000000000000000000000000000000000000..8ee7b130ce917183edfa26a7a53c1a8df1353303 --- /dev/null +++ b/nixos/modules/monitoring/server/grafana-config/services-overview.json @@ -0,0 +1,664 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "RED: Requests-Errors-Duration for our services", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 18, + "panels": [], + "title": "Payments v1/stripe/charge", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "HTTPS responses per second", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(http_responses_total{path=\"v1/stripe/charge\"}[5m])", + "instant": false, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/stripe/charge RPS", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": true, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"4XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "legendFormat": "Client error (4XX) rate", + "refId": "A" + }, + { + "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"5XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "legendFormat": "Server error (5XX) rate", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/stripe/charge error rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percentunit", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "Requests taking longer than 1 s, between 1 sec and 10 msec, and 10 msec and below", + "fill": 2, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.01\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "=< 0.01s", + "refId": "A" + }, + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.01\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "=< 1s", + "refId": "D" + }, + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "> 1s", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/stripe/charge durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 11, + "panels": [], + "title": "Payments v1/redeem", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "HTTPS responses per second", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(http_responses_total{path=\"v1/redeem\"}[5m])", + "instant": false, + "intervalFactor": 1, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/redeem RPS", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 9 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": true, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(http_responses_total{path=\"v1/redeem\", status=\"4XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "legendFormat": "Client error (4XX) rate", + "refId": "A" + }, + { + "expr": "sum(http_responses_total{path=\"v1/redeem\", status=\"5XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "legendFormat": "Server error (5XX) rate", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/redeem error rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percentunit", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "Requests taking longer than 1 s, between 1 sec and 10 msec, and 10 msec and below", + "fill": 2, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.01\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "=< 0.01s", + "refId": "A" + }, + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.01\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "=< 1s", + "refId": "D" + }, + { + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "> 1s", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "v1/redeem durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "", + "schemaVersion": 20, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Services overview", + "uid": "JX3RVEk7k", + "version": 6 +} diff --git a/nixos/modules/monitoring/server/grafana.nix b/nixos/modules/monitoring/server/grafana.nix index d5724e7188cab5155d7f1976420185388caf5d64..2fd9e7f7c83217afc4943e644f6d3161e56c49f9 100644 --- a/nixos/modules/monitoring/server/grafana.nix +++ b/nixos/modules/monitoring/server/grafana.nix @@ -7,6 +7,16 @@ let cfg = config.services.private-storage.monitoring.grafana; + grafanaAuth = if (cfg.googleOAuthClientID == "") then { + anonymous.enable = true; + } else { + google.enable = true; + # Grafana considers it "sign up" to let in a user it has + # never seen before. + google.allowSignUp = true; + google.clientSecretFile = cfg.googleOAuthClientSecretFile; + google.clientId = cfg.googleOAuthClientID; + }; in { options.services.private-storage.monitoring.grafana = { @@ -18,19 +28,45 @@ in { prometheusUrl = lib.mkOption { type = lib.types.str; example = lib.literalExample "http://prometheus:9090/"; - default = "http://prometheus:9090/"; + default = "http://localhost:9090/"; description = "The URL of the Prometheus host to access"; }; lokiUrl = lib.mkOption { type = lib.types.str; example = lib.literalExample "http://loki:3100/"; - default = "http://loki:3100/"; + default = "http://localhost:3100/"; description = "The URL of the Loki host to access"; }; + letsEncryptAdminEmail = lib.mkOption + { type = lib.types.str; + description = '' + An email address to give to Let's Encrypt as an + operational contact for the service's TLS certificate. + ''; + }; + googleOAuthClientID = lib.mkOption + { type = lib.types.str; + example = lib.literalExample "grafana-staging-345678"; + default = "replace-by-your-client-id-or-set-empty-string-for-anonymous-access"; + description = "The GSuite OAuth2 SSO Client ID. Empty string turns SSO auth off and anonymous (free for all) access on."; + }; + googleOAuthClientSecretFile = lib.mkOption + { type = lib.types.path; + example = lib.literalExample "/var/secret/monitoring-gsuite-client-secret"; + default = /run/keys/grafana-google-sso.secret; + description = "The path to the GSuite SSO secret file."; + }; + adminPasswordFile = lib.mkOption + { type = lib.types.path; + example = lib.literalExample "/var/secret/monitoring-admin-password"; + default = /run/keys/grafana-admin.password; + description = "A file containing the password for the Grafana Admin account."; + }; }; config = { - # networking.firewall.allowedTCPPorts = [ 80 443 ]; + # Port 80 for ACME ssl retrieval only. 443 for nginx -> grafana. + networking.firewall.allowedTCPPorts = [ 80 443 ]; services.grafana = { enable = true; @@ -38,42 +74,79 @@ in { port = 2342; addr = "127.0.0.1"; - # All three are required to forego the user/pass prompt: - auth.anonymous.enable = true; - auth.anonymous.org_role = "Admin"; - auth.anonymous.org_name = "Main Org."; - }; + # No phoning home + analytics.reporting.enable = false; - services.grafana.provision = { - enable = true; - # See https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources - datasources = [{ - name = "Prometheus"; - type = "prometheus"; - access = "proxy"; - url = cfg.prometheusUrl; - isDefault = true; - } { - name = "Loki"; - type = "loki"; - access = "proxy"; - url = cfg.lokiUrl; - }]; - # See https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards - dashboards = [{ - name = "provisioned"; - options.path = ./grafana-config; - }]; + # Force Grafana to believe it is reachable via https on the default port + # number because that's where the nginx that forwards traffic to it is + # listening. Grafana's own server listens on an internal address that + # doesn't matter to anyone except our nginx instance. + rootUrl = "https://%(domain)s/"; + + extraOptions = { + # Defend against DNS rebinding attacks. + SERVER_ENFORCE_DOMAIN = "true"; + }; + + auth = { + anonymous.org_role = "Admin"; + anonymous.org_name = "Main Org."; + } // grafanaAuth; + + # Give users that come through GSuite SSO the highest possible privileges: + users.autoAssignOrgRole = "Editor"; + + # Read the admin password from a file in our secrets folder: + security.adminPasswordFile = cfg.adminPasswordFile; + + provision = { + enable = true; + # See https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources + datasources = [{ + name = "Prometheus"; + type = "prometheus"; + access = "proxy"; + url = cfg.prometheusUrl; + isDefault = true; + } { + name = "Loki"; + type = "loki"; + access = "proxy"; + url = cfg.lokiUrl; + }]; + # See https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards + dashboards = [{ + name = "provisioned"; + options.path = ./grafana-config; + }]; + }; }; # nginx reverse proxy - services.nginx.enable = true; - services.nginx.virtualHosts.${config.services.grafana.domain} = { - locations."/" = { - proxyPass = "http://127.0.0.1:${toString config.services.grafana.port}"; - proxyWebsockets = true; + security.acme.email = cfg.letsEncryptAdminEmail; + security.acme.acceptTerms = true; + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + # Only allow PFS-enabled ciphers with AES256: + sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; + + virtualHosts.${config.services.grafana.domain} = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString config.services.grafana.port}"; + proxyWebsockets = true; + }; }; }; + + # Let Grafana read from keys, if necessary. + users.users.grafana.extraGroups = [ "keys" ]; }; } - diff --git a/nixos/modules/monitoring/server/prometheus.nix b/nixos/modules/monitoring/server/prometheus.nix index 36c2ba6402559771dff8771b1369842e21f7ff7f..c92261ccad2ebdd8dd34dda9027e66962b345be9 100644 --- a/nixos/modules/monitoring/server/prometheus.nix +++ b/nixos/modules/monitoring/server/prometheus.nix @@ -26,6 +26,11 @@ in { example = lib.literalExample "[ node1 node2 ]"; description = "List of nodes (hostnames or IPs) to scrape."; }; + paymentExporterTargets = lib.mkOption { + type = with lib.types; listOf str; + example = lib.literalExample "[ node1 node2 ]"; + description = "List of nodes (hostnames or IPs) to scrape."; + }; }; config = rec { @@ -49,6 +54,15 @@ in { }]; relabel_configs = [ dropPortNumber ]; } + { + job_name = "payment-exporters"; + scheme = "https"; + tls_config.insecure_skip_verify = true; + static_configs = [{ + targets = cfg.paymentExporterTargets; + }]; + relabel_configs = [ dropPortNumber ]; + } ]; }; }; diff --git a/nixos/modules/monitoring/vpn/client.nix b/nixos/modules/monitoring/vpn/client.nix index dbd50b82ef5b09495e332e6fbb7ac5676f5ac322..ed1933e34d715fba0933f32d606e989b4d1ed4ec 100644 --- a/nixos/modules/monitoring/vpn/client.nix +++ b/nixos/modules/monitoring/vpn/client.nix @@ -48,7 +48,7 @@ in { }; endpointPublicKeyFile = lib.mkOption { type = lib.types.path; - example = lib.literalExample ../PrivateStorageSecrets/monitoringvpn/server.pub; + example = lib.literalExample ./monitoringvpn/server.pub; description = '' File with base64 public key generated by <command>cat private.key | wg pubkey > pubkey.pub</command>. ''; @@ -71,4 +71,3 @@ in { }; }; } - diff --git a/nixos/modules/monitoring/vpn/server.nix b/nixos/modules/monitoring/vpn/server.nix index 2374ddc8657fb299fb83155cbabe328cd54c1aaf..3c41e0209bb7fe18f1a81a44ab509c8442372bbf 100644 --- a/nixos/modules/monitoring/vpn/server.nix +++ b/nixos/modules/monitoring/vpn/server.nix @@ -51,7 +51,7 @@ in { }; pubKeysPath = lib.mkOption { type = lib.types.path; - example = lib.literalExample ../PrivateStorageSecrets/monitoringvpn; + example = lib.literalExample ./monitoringvpn; description = '' The path to the directory that holds the public keys. ''; @@ -69,4 +69,3 @@ in { }; }; } - diff --git a/nixos/modules/private-storage.nix b/nixos/modules/private-storage.nix index 52720e618973c57b41aade87585c7ab758abff22..fa5fea837c544e66ae8811a2e3c468a67a18759e 100644 --- a/nixos/modules/private-storage.nix +++ b/nixos/modules/private-storage.nix @@ -8,6 +8,9 @@ let # TODO: This path copied from tahoe.nix. tahoe-base = "/var/db/tahoe-lafs"; + # Our own nixpkgs fork: + ourpkgs = import ../../nixpkgs-ps.nix {}; + # The full path to the directory where the storage server will write # incident reports. incidents-dir = "${tahoe-base}/${storage-node-name}/logs/incidents"; @@ -18,6 +21,12 @@ let # NOTE: This is promised by the service privacy policy. It *may not* be # raised without following the process for updating the privacy policy. max-incident-age = "29d"; + + fqdn = "${ + assert config.networking.hostName != null; config.networking.hostName + }.${ + assert config.networking.domain != null; config.networking.domain + }"; in { imports = [ @@ -31,19 +40,20 @@ in options = { services.private-storage.enable = lib.mkEnableOption "private storage service"; services.private-storage.tahoe.package = lib.mkOption - { default = pkgs.privatestorage; + { default = ourpkgs.privatestorage; type = lib.types.package; example = lib.literalExample "pkgs.tahoelafs"; description = '' The package to use for the Tahoe-LAFS daemon. ''; }; - services.private-storage.publicIPv4 = lib.mkOption - { default = "127.0.0.1"; + services.private-storage.publicAddress = lib.mkOption + { default = "${fqdn}"; type = lib.types.str; - example = lib.literalExample "192.0.2.0"; + example = lib.literalExample "storage.example.invalid"; description = '' - An IPv4 address to advertise for this storage service. + A publicly-visible address to use in Tahoe-LAFS advertisements for + this storage service. ''; }; services.private-storage.introducerFURL = lib.mkOption @@ -63,7 +73,7 @@ in ''; }; services.private-storage.issuerRootURL = lib.mkOption - { default = "https://issuer.privatestorage.io/"; + { default = "https://issuer.${config.networking.domain}/"; type = lib.types.str; example = lib.literalExample "https://example.invalid/"; description = '' @@ -122,7 +132,7 @@ in # First, in the syntax which it uses to listen. "tub.port" = "tcp:${toString cfg.publicStoragePort}"; # Second, in the syntax it advertises to in the fURL. - "tub.location" = "tcp:${cfg.publicIPv4}:${toString cfg.publicStoragePort}"; + "tub.location" = "tcp:${cfg.publicAddress}:${toString cfg.publicStoragePort}"; }; storage = { enabled = true; @@ -153,7 +163,7 @@ in environment.systemPackages = [ # Provide a useful tool for reporting about shares. - pkgs.leasereport + ourpkgs.leasereport ]; }; diff --git a/nixos/modules/ssh.nix b/nixos/modules/ssh.nix index 667bdd26215b4e0978781244741dd4c5313cefbd..3e90528322c153d6b96679af5d914c4e753b49bf 100644 --- a/nixos/modules/ssh.nix +++ b/nixos/modules/ssh.nix @@ -40,12 +40,6 @@ # Agent forwarding is fraught. It can be used by an attacker to # leverage one compromised system into more. Discourage its use. AllowAgentForwarding no - - # Only allow authentication as one of the configured users, not random - # other (often system-managed) users. Possibly this is also - # superfluous! NixOS system users have nologin as their shell ... so they - # cannot log in anyway. - AllowUsers ${builtins.concatStringsSep " " (builtins.attrNames cfg.sshUsers)} ''; }; diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix index cbf4c5937ca6780ce9e931d6ceec91c29643fbc3..353abc891fafd1cc988e47a1befa530a012470dc 100644 --- a/nixos/modules/tests/private-storage.nix +++ b/nixos/modules/tests/private-storage.nix @@ -115,7 +115,7 @@ in { ]; services.private-storage = { enable = true; - publicIPv4 = "storage"; + publicAddress = "storage"; introducerFURL = introducerFURL; issuerRootURL = issuerURL; inherit ristrettoSigningKeyPath; diff --git a/nixos/modules/update-deployment b/nixos/modules/update-deployment new file mode 100755 index 0000000000000000000000000000000000000000..d8d32ff64eb52123be448ed598d00ab2bc1850da --- /dev/null +++ b/nixos/modules/update-deployment @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +# Accept the name of the grid this system is part of as a parameter. This +# lets us pick the correct morph grid source file later on. +GRIDNAME=$1 +shift + +# Determine the right branch name to use for the particular grid we've been +# told we belong to. The grid name is a parameter to this script we can +# re-use it across all of our grids. See deployment.nix for the ssh +# configuration that controls what value is actually passed when an update is +# triggered. +case "${GRIDNAME}" in + "local") + BRANCH="develop" + ;; + + "testing") + BRANCH="staging" + ;; + + "production") + BRANCH="production" + ;; + + *) + echo "Unknown grid: ${GRIDNAME}" + exit 1 +esac + +# This is where we will maintain a checkout of PrivateStorageio for morph to +# use to compute the desired state. +CHECKOUT="${HOME}/PrivateStorageio" + +# This is the address of the git remote where we can get the latest +# PrivateStorageio. +REPO="https://whetstone.privatestorage.io/privatestorage/PrivateStorageio.git" + +if [ -e "${CHECKOUT}" ]; then + # It exists already so just make sure it contains the latest changes from + # the canonical repository. + git -C "${CHECKOUT}" fetch +else + # It doesn't exist so clone it. + git clone "${REPO}" "${CHECKOUT}" +fi + +# Get us to a pristine checkout of the right branch. +git -C "${CHECKOUT}" reset --hard "origin/${BRANCH}" + +# If we happen to be on the local grid then fix the undefined key. +if [ "${GRIDNAME}" = "local" ]; then + KEY="$(cat /etc/ssh/authorized_keys.d/vagrant)" + sed -i "s_undefined_\"${KEY}\"_" "${CHECKOUT}"/morph/grid/${GRIDNAME}/public-keys/users.nix +fi + +# Compute a log message explaining what we're doing. +LOG_MESSAGE="$(date --iso-8601=seconds) $(git -C "${CHECKOUT}" rev-parse HEAD)" + +# Make sure we use the right credentials and ask for the right account when +# morph makes the connection. morph's deployment target for each host is the +# full domain name (even though the host is only named with the unqualified +# hostname in the morph grid definition) so compute an ssh config section that +# matches that. Regardless, point this effort at localhost because we *know* +# it's just us we want to update. +cat > ~/.ssh/config <<EOF +Host $(hostname).$(domainname) + HostName 127.0.0.1 + IdentityFile ~/.ssh/morph_key + User root +EOF + +# Make sure known_hosts has the host key in it. +ssh -o StrictHostKeyChecking=no "$(hostname).$(domainname)" ":" + +# Set nixpkgs to our preferred version for the morph build. Annoyingly, we +# can't just use nixpkgs-2105.nix as our nixpkgs because some code (in morph, +# at least) wants <nixpkgs> to be a fully-resolved path to a nixpkgs tree. +# For example, morph evaluated `import <nixpkgs/lib>` which would turn into +# something like `import nixpkgs-2105.nix/lib` which is nonsense. +# +# So instead, import our nixpkgs which forces it to be instantiated in the +# store, then ask for its path, then set NIX_PATH to that. +export NIX_PATH="nixpkgs=$(nix eval "(import ${CHECKOUT}/nixpkgs-2105.nix { }).path")" + +# Attempt to update just this host. Choose the morph grid definition matching +# the grid we belong to and limit the morph deployment update to the host +# matching our name. morph uses just the bare hostname without the domain +# part. +if morph deploy "${CHECKOUT}"/morph/grid/"${GRIDNAME}"/grid.nix switch --on "$(hostname)"; then + # The deployment succeeded. Record success along with context we pre-computed. + echo "${LOG_MESSAGE} OK" >> ${HOME}/updates.txt + exit 0 +else + # Oops. Not so fortunate. Record failure. + echo "${LOG_MESSAGE} FAIL" >> ${HOME}/updates.txt + exit 1 +fi diff --git a/nixos/system-tests.nix b/nixos/system-tests.nix index b2556d4692ee0c3eff96554fa7c13df59ec94507..5f51d01dd57267b75b3742c76c03c1393676d426 100644 --- a/nixos/system-tests.nix +++ b/nixos/system-tests.nix @@ -1,6 +1,6 @@ # The overall system test suite for PrivateStorageio NixOS configuration. let - pkgs = import <nixpkgs> { }; + pkgs = import ../nixpkgs-ps.nix { }; in { private-storage = pkgs.nixosTest ./modules/tests/private-storage.nix; tahoe = pkgs.nixosTest ./modules/tests/tahoe.nix; diff --git a/nixpkgs-2105.json b/nixpkgs-2105.json index d441d00995a78a20cc8677a85ced2a675a9502ae..76950db1870cb62d68e655f5ca4be90f3fcbf6be 100644 --- a/nixpkgs-2105.json +++ b/nixpkgs-2105.json @@ -1,4 +1,4 @@ -{ "name": "stable2105" +{ "name": "release2105" , "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.804.5de44c15758/nixexprs.tar.xz" , "sha256": "002zvc16hyrbs0icx1qj255c9dqjpdxx4bhhfjndlj3kwn40by0m" } diff --git a/nixpkgs-2105.nix b/nixpkgs-2105.nix new file mode 100644 index 0000000000000000000000000000000000000000..536d913b89ba6a57d8d683381ea1c8f40e026b4f --- /dev/null +++ b/nixpkgs-2105.nix @@ -0,0 +1 @@ +import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs-2105.json))) diff --git a/nixpkgs-ps.json b/nixpkgs-ps.json new file mode 100644 index 0000000000000000000000000000000000000000..97449535de722fdaf89d8551c6bd6d124e3818c7 --- /dev/null +++ b/nixpkgs-ps.json @@ -0,0 +1,4 @@ +{ "name": "nixpkgs" +, "url": "https://github.com/PrivateStorageio/nixpkgs/archive/a5cbaadd9676e8c568061e92bbf5ad6a5d884ded.tar.gz" +, "sha256": "0q5zknsp0qb25ag9zr9bw1ap7pb3f76bxsw81ahxkzj4z5dw6k2f" +} diff --git a/nixpkgs-ps.nix b/nixpkgs-ps.nix new file mode 100644 index 0000000000000000000000000000000000000000..d98a53843052fda824f4ed7e34db50524df36ce2 --- /dev/null +++ b/nixpkgs-ps.nix @@ -0,0 +1 @@ +import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs-ps.json))) diff --git a/nixpkgs.json b/nixpkgs.json deleted file mode 100644 index 33b343ef3498ae226218f59be257e808e9a88c7e..0000000000000000000000000000000000000000 --- a/nixpkgs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ "name": "nixpkgs" -, "url": "https://github.com/PrivateStorageio/nixpkgs/archive/8c7a61c658e32eaccf666e5fe818a996c36a988f.tar.gz" -, "sha256": "1ln0a8c20qykm57wl901lixny1fcfmzgbavd7pbjk6jbnfij59bl" -} diff --git a/shell.nix b/shell.nix index 2c1c5123da656d34fafe0883b50ef49c578c6c8b..f3d2750edd68e4861e6d0700e0259c1ce86f817a 100644 --- a/shell.nix +++ b/shell.nix @@ -1,13 +1,12 @@ let - nixpkgs = import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs.json))) { }; - stable2105 = import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs-2105.json))) { }; + release2105 = import ./nixpkgs-2105.nix { }; in -{ pkgs ? nixpkgs }: +{ pkgs ? release2105 }: pkgs.mkShell { - NIX_PATH = "nixpkgs=${nixpkgs.path}"; + NIX_PATH = "nixpkgs=${pkgs.path}"; buildInputs = [ pkgs.morph - stable2105.vagrant + pkgs.vagrant pkgs.jp ]; } diff --git a/tools/create-vpn-keys.sh b/tools/create-vpn-keys.sh index e092a8ced698bd3a3bb2d4acc3ca07a3a8e6032d..c81225ec340db38d551e9d0c6c13d7bd44e21a4d 100755 --- a/tools/create-vpn-keys.sh +++ b/tools/create-vpn-keys.sh @@ -4,7 +4,7 @@ # Parameters: # file: path to grid.nix of morph deployment # -# Output: Key files for all monitoring VPN hosts in secrets/monitoringvpn +# Output: Key files for all monitoring VPN hosts in {private,public}-keys/monitoringvpn # relative to the grid.nix # # The server key will also be symlinked to server.{key,pub}. @@ -19,7 +19,8 @@ if [[ $# -ne 1 ]]; then fi SRC=$(dirname $0) -VPN_SECRETS=$(dirname $1)/secrets/monitoringvpn +VPN_SECRETS=$(dirname $1)/private-keys/monitoringvpn +VPN_PUBLIC=$(dirname $1)/public-keys/monitoringvpn CONFIG=$(nix-instantiate --strict --json --eval "${SRC}"/get-vpn-config.nix --arg pathToGrid "${1}") @@ -27,14 +28,15 @@ MONITORING_IPS=$(echo $CONFIG | jp --unquoted "join(' ', clientIPs)") VPNSERVER_IP=$(echo $CONFIG | jp --unquoted "serverIP") mkdir -p "${VPN_SECRETS}" +mkdir -p "${VPN_PUBLIC}" for i in $MONITORING_IPS $VPNSERVER_IP; do - wg genkey | tee "${VPN_SECRETS}"/${i}.key | wg pubkey > "${VPN_SECRETS}"/${i}.pub + wg genkey | tee "${VPN_SECRETS}"/${i}.key | wg pubkey > "${VPN_PUBLIC}"/${i}.pub done wg genpsk > "${VPN_SECRETS}"/preshared.key ln -fs $VPNSERVER_IP.key "${VPN_SECRETS}"/server.key -ln -fs $VPNSERVER_IP.pub "${VPN_SECRETS}"/server.pub +ln -fs $VPNSERVER_IP.pub "${VPN_PUBLIC}"/server.pub # EOF