Compare commits

..

1 commit

Author SHA1 Message Date
ibizaman
76a07d5edf wip lldap declarative 2024-06-10 08:38:01 -07:00
223 changed files with 8834 additions and 47532 deletions

View file

@ -1,9 +0,0 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- ibizaman

View file

@ -23,28 +23,15 @@ on:
required: false required: false
jobs: jobs:
automerge-rebase: automerge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: reitermarkus/automerge@v2 - uses: reitermarkus/automerge@v2
with: with:
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }} token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
merge-method: rebase merge-method: squash
do-not-merge-labels: never-merge do-not-merge-labels: never-merge
required-labels: automerge required-labels: automerge
pull-request: ${{ github.event.inputs.pull-request }} pull-request: ${{ github.event.inputs.pull-request }}
review: ${{ github.event.inputs.review }} review: ${{ github.event.inputs.review }}
dry-run: false dry-run: false
automerge-merge:
runs-on: ubuntu-latest
steps:
- uses: reitermarkus/automerge@v2
with:
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
merge-method: merge
do-not-merge-labels: never-merge
required-labels: automerge-merge
pull-request: ${{ github.event.inputs.pull-request }}
review: ${{ github.event.inputs.review }}
dry-run: false

View file

@ -1,159 +0,0 @@
# name: build
# on: push
# jobs:
# checks:
# uses: nixbuild/nixbuild-action/.github/workflows/ci-workflow.yml@v19
# with:
# nix_conf: |
# allow-import-from-derivation = true
# secrets:
# nixbuild_token: ${{ secrets.nixbuild_token }}
name: "build"
on:
pull_request:
push:
branches: [ "main" ]
jobs:
path-filter:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.filter.outputs.any_changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: tj-actions/changed-files@v47
id: filter
with:
files: |
lib/**
modules/**
!modules/**/docs/**
test/**
flake.lock
flake.nix
.github/workflows/build.yaml
separator: "\n"
- env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_changed_files }}
run: |
echo $ALL_CHANGED_FILES
build-matrix:
needs: [ "path-filter" ]
if: needs.path-filter.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
enable_kvm: true
extra_nix_config: |
keep-outputs = true
keep-failed = true
- name: Setup Caching
uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Generate Matrix
id: generate-matrix
run: |
set -euox pipefail
nix flake show --allow-import-from-derivation --json \
| jq -c '.["checks"]["x86_64-linux"] | keys' > .output
cat .output
echo dynamic_list="$(cat .output)" >> "$GITHUB_OUTPUT"
outputs:
check: ${{ steps.generate-matrix.outputs.dynamic_list }}
manual:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
enable_kvm: true
extra_nix_config: |
keep-outputs = true
keep-failed = true
- name: Setup Caching
uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
run: |
nix \
--print-build-logs \
--option keep-going true \
--show-trace \
build .#manualHtml
tests:
runs-on: ubuntu-latest
needs: [ "build-matrix" ]
strategy:
fail-fast: false
matrix:
check: ${{ fromJson(needs.build-matrix.outputs.check) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
enable_kvm: true
extra_nix_config: |
keep-outputs = true
keep-failed = true
- name: Setup Caching
uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
run: |
echo "resultPath=$(nix eval .#checks.x86_64-linux.${{ matrix.check }} --raw)" >> $GITHUB_ENV
nix build --print-build-logs --show-trace .#checks.x86_64-linux.${{ matrix.check }}
- name: Upload Build Result
uses: actions/upload-artifact@v7
if: always() && startsWith(matrix.check, 'vm_')
with:
name: ${{ matrix.check }}
path: ${{ env.resultPath }}/trace/*
overwrite: true
if-no-files-found: ignore
results:
name: Final Results
runs-on: ubuntu-latest
needs: [ manual, tests ]
if: '!cancelled()'
steps:
- run: |
result="${{ needs.manual.result }}"
if ! [[ $result == "success" || $result == "skipped" ]]; then
exit 1
fi
result="${{ needs.tests.result }}"
if ! [[ $result == "success" || $result == "skipped" ]]; then
exit 1
fi
exit 0

View file

@ -1,111 +0,0 @@
name: Demo
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
jobs:
path-filter:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.filter.outputs.any_changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: tj-actions/changed-files@v47
id: filter
with:
files: |
demo/**
lib/**
modules/**
!modules/**/docs/**
test/**
flake.lock
flake.nix
.github/workflows/demo.yml
separator: "\n"
- env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_changed_files }}
run: |
echo $ALL_CHANGED_FILES
build:
needs: [ "path-filter" ]
if: needs.path-filter.outputs.changed == 'true'
strategy:
fail-fast: false
matrix:
demo:
- name: homeassistant
flake: basic
- name: homeassistant
flake: ldap
- name: nextcloud
flake: basic
- name: nextcloud
flake: ldap
- name: nextcloud
flake: sso
- name: minimal
flake: minimal
- name: minimal
flake: lowlevel
- name: minimal
flake: sops
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
enable_kvm: true
extra_nix_config: |
keep-outputs = true
keep-failed = true
- uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build ${{ matrix.demo.name }} .#${{ matrix.demo.flake }}
run: |
cd demo/${{ matrix.demo.name }}
nix flake update --override-input selfhostblocks ../.. selfhostblocks
nix \
--print-build-logs \
--option keep-going true \
--show-trace \
build .#nixosConfigurations.${{ matrix.demo.flake }}.config.system.build.toplevel
nix \
--print-build-logs \
--option keep-going true \
--show-trace \
build .#nixosConfigurations.${{ matrix.demo.flake }}.config.system.build.vm
result:
runs-on: ubuntu-latest
needs: [ "build" ]
if: '!cancelled()'
steps:
- run: |
result="${{ needs.build.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi

View file

@ -1,24 +0,0 @@
name: "format"
on:
pull_request:
push:
branches: [ "main" ]
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Caching
uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Check Formatting
run: |
find . -name '*.nix' | nix fmt -- --ci

20
.github/workflows/garnix.yaml vendored Normal file
View file

@ -0,0 +1,20 @@
name: "Final Results"
on:
check_suite:
types: [completed]
jobs:
results:
name: Final Results
runs-on: ubuntu-latest
steps:
- run: echo
- run: exit 1
# see https://stackoverflow.com/a/67532120/4907315
if: >-
${{
contains(needs.*.result, 'failure')
|| contains(needs.*.result, 'cancelled')
|| contains(needs.*.result, 'skipped')
}}

View file

@ -1,43 +1,24 @@
# See shb.skarabox.com/misc.html
name: Update Flake Lock name: Update Flake Lock
on: on:
schedule:
- cron: '0 */3 * * *' # runs every 3 hours at :00
workflow_dispatch: workflow_dispatch:
inputs: schedule:
bisect_future: - cron: '0 0 * * *' # runs daily at 00:00
description: "Bisect in the future instead of the past"
required: false
default: "false"
jobs: jobs:
lockfile: lockfile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v3
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: DeterminateSystems/nix-installer-action@main
with: with:
github_access_token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Cache nixpkgs mirror extra-conf: "system-features = nixos-test benchmark big-parallel kvm"
uses: actions/cache@v5
with:
path: .cache/nixpkgs.git
key: nixpkgs-mirror-v1
- name: Update flake.lock - name: Update flake.lock
env: uses: DeterminateSystems/update-flake-lock@main
GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }} with:
BISECT_FUTURE: ${{ inputs.bisect_future }} token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
run: | pr-labels: |
git config user.email "update-flake-lock-pr@selfhostblocks.com" automerge
git config user.name "Update Flake Lock PR"
if [ "$BISECT_FUTURE" = false ]; then
BISECT_FUTURE=
elif [ -n "$BISECT_FUTURE" ]; then
BISECT_FUTURE=future
fi
nix run .#update-flake-lock-pr $BISECT_FUTURE

View file

@ -2,8 +2,14 @@
name: Deploy docs name: Deploy docs
on: on:
# Runs on pushes targeting the default branch
push: push:
branches: ["main"] branches: ["main"]
# TODO: needed ?
# schedule:
# - cron: 0 0 * * 1
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@ -29,18 +35,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v3
- name: Install Nix - name: Install nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v20
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Caching
uses: cachix/cachix-action@v17
with:
name: selfhostblocks
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build docs - name: Build docs
run: | run: |
@ -59,13 +57,13 @@ jobs:
public public
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v6 uses: actions/configure-pages@v3
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v5 uses: actions/upload-pages-artifact@v1
with: with:
path: ./public path: ./public
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v5 uses: actions/deploy-pages@v2

View file

@ -1,223 +0,0 @@
{
gh,
writeShellApplication,
}:
writeShellApplication {
name = "update-flake-lock-pr";
runtimeInputs = [
gh
];
text = ''
branch=ci/nixpkgs-update
if [ "''${GITHUB_EVENT_NAME:-}" = "schedule" ]; then
branch=ci/nixpkgs-update-auto
fi
goto_future=no
if [ -n "''${1:-}" ]; then
goto_future=yes
fi
main () {
PR=$(get_pr)
if [ -z "$PR" ]; then
create_pr
exit 0
fi
case "$(get_pr_check_status "$PR")" in
pending)
echo "Checks are running on PR $PR, nothing to do yet."
exit 0
;;
succeeded)
echo "Checks succeeded, nothing to do."
exit 0
;;
failing)
echo "Checks failed, updating PR to midpoint commit."
local CURRENT_COMMIT
local LAST_WORKING_COMMIT
local FAILING_COMMIT
local NEW_COMMIT
CURRENT_COMMIT="$(get_main_branch_commit)"
if [ "$goto_future" = "no" ]; then
LAST_WORKING_COMMIT="$CURRENT_COMMIT"
FAILING_COMMIT="$(get_pr_commit "$PR")"
else
LAST_WORKING_COMMIT="$(get_pr_commit "$PR")"
FAILING_COMMIT="$(get_latest_nixpkgs_commit)"
fi
NEW_COMMIT="$(midpoint_commit "$LAST_WORKING_COMMIT" "$FAILING_COMMIT")"
if [ "$NEW_COMMIT" = "$LAST_WORKING_COMMIT" ] || [ "$NEW_COMMIT" = "$FAILING_COMMIT" ]; then
NEW_COMMIT="$(get_latest_nixpkgs_commit)"
fi
if [ "$NEW_COMMIT" = "$FAILING_COMMIT" ]; then
echo "Latest nixpkgs commit is still same failing commit, will retry later."
exit 0
fi
update_pr "$PR" "$CURRENT_COMMIT" "$NEW_COMMIT"
exit 0
;;
esac
}
get_main_branch_commit() {
git fetch origin main
git show origin/main:flake.lock \
| jq -r '.nodes.nixpkgs.locked.rev'
}
midpoint_commit() {
local good="$1"
local bad="$2"
mkdir -p .cache
if [ ! -d .cache/nixpkgs.git ]; then
git clone --mirror https://github.com/NixOS/nixpkgs.git .cache/nixpkgs.git
else
git --git-dir=.cache/nixpkgs.git fetch origin
fi
local commits=()
mapfile -t commits < <(
git --git-dir=.cache/nixpkgs.git \
rev-list \
--reverse \
--ancestry-path \
"''${good}..''${bad}"
)
local count="''${#commits[@]}"
if [ "$count" -eq 0 ]; then
echo "$bad"
return
fi
local midpoint_index=$((count / 2))
echo "''${commits[$midpoint_index]}"
}
get_latest_nixpkgs_commit () {
git ls-remote https://github.com/NixOS/nixpkgs nixos-unstable | cut -f1
}
get_pr () {
gh pr list \
--head "$branch" \
--state open \
--json number \
--jq '.[0].number // empty'
}
create_pr() {
local test_commit
test_commit="$(get_latest_nixpkgs_commit)"
echo "Creating PR on nixpkgs' latest commit $test_commit"
git checkout -B "$branch"
nix flake lock \
--override-input nixpkgs "github:NixOS/nixpkgs/$test_commit"
git add flake.lock
git commit -m "update nixpkgs to $test_commit"
git push -u origin "$branch" --force
current_commit="$(get_main_branch_commit)"
gh pr create \
--title "update nixpkgs to $test_commit" \
--body "$(printf "%s\n\n%s" "Automated nixpkgs update. Latest tries:" " - https://github.com/NixOS/nixpkgs/compare/$current_commit...$test_commit")" \
--label automerge-merge
}
append_pr_body() {
local pr="$1"
local text="$2"
local current_body
current_body="$(gh pr view "$pr" --json body --jq .body)"
gh pr edit "$pr" \
--body "$(printf '%s%b' "$current_body" "$text")"
}
update_pr() {
local pr="$1"
local current_commit="$2"
local test_commit="$3"
echo "Updating PR to nixpkgs' commit $test_commit"
git checkout "$branch"
nix flake lock \
--override-input nixpkgs "github:NixOS/nixpkgs/$test_commit"
git add flake.lock
git commit -m "update nixpkgs to $test_commit"
git push --force-with-lease origin "$branch"
local future_text
if [ "$goto_future" = "yes" ]; then
future_text="bisected in the future"
else
future_text="bisected in the past"
fi
gh pr edit "$pr" \
--title "update nixpkgs to $test_commit"
append_pr_body "$pr" \
" \n - https://github.com/NixOS/nixpkgs/compare/$current_commit...$test_commit ($future_text)"
}
get_pr_check_status() {
local pr="$1"
local status
status="$(gh pr checks "$pr" --json state --jq '.[].state' || true)"
if echo "$status" | grep -qE 'PENDING|QUEUED|IN_PROGRESS'; then
echo "pending"
return
fi
if echo "$status" | grep -qE 'FAILURE|TIMED_OUT|CANCELLED'; then
echo "failing"
return
fi
echo "success"
}
get_pr_commit() {
local pr="$1"
git fetch origin "$branch"
git show "origin/$branch:flake.lock" \
| jq -r '.nodes.nixpkgs.locked.rev'
}
main
'';
}

View file

@ -1,22 +0,0 @@
name: Version Bump
on:
push:
branches:
- main
paths:
- VERSION
jobs:
create-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Get version
id: vars
run: echo "version=v$(cat VERSION)" >> $GITHUB_OUTPUT
- uses: rickstaa/action-create-tag@v1.7.2
with:
tag: ${{ steps.vars.outputs.version }}

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
*.qcow2 *.qcow2
result result
result-* result-*
docs/redirects.json.backup
.nixos-test-history
\#*#

View file

@ -1,496 +1,16 @@
<!---
Template:
## Breaking Changes
## New Features
## User Facing Backwards Compatible Changes
## Fixes
## Other Changes
-->
# Upcoming Release # Upcoming Release
# v0.9.0
Commits: https://github.com/ibizaman/selfhostblocks/compare/v0.8.0...v0.9.0
## Breaking Changes ## Breaking Changes
- Updated simple-nixos-mailserver. Update all options following this pattern:
```diff
- mailserver.mailboxes.<name>.specialUse = "value"
+ mailserver.mailboxes.<name>.special_use = "\\value"
```
Note that simple-nixos-mailserver wanted to upgrade the location of the storage path
to include the ldap UID instead of the email but I kept the email address.
This means there is no migration to do.
- Update nixpkgs [from 6201e2 to abd1ea](https://github.com/ibizaman/selfhostblocks/compare/6201e203d09599479a3b3450ed24fa81537ebc4e...abd1ea17fdbedd48cbca770847b39e3a7a09ab5b).
## New Features
- Add `shb.zfs.snapshotBeforeActivation` option to take a ZFS snapshot of given datasets before activation.
See [the manual](https://shb.skarabox.com/blocks-zfs.html) for more details.
- Add [sanoid block](https://shb.skarabox.com/blocks-sanoid.html)
- Add option to take snapshot before activation with ZFS block.
## Fixes
- Fix oidc_login build for Nextcloud
- Fix mailserver build
## Other Changes
- Harden lldap configuration
- Convert ZFS block to system and [add documentation](https://shb.skarabox.com/blocks-zfs.html)
- Switch monitoring log fetching from deprecated promtail to fluent-bit
- Add test to catch drift for Let's Encrypt DNS provider
# [BROKEN] v0.8.0
## Breaking Changes
- Bump of Nextcloud version to 32 and 33 because of nixpkgs bump. All provided apps are verified compatible with Nextcloud 33 thanks to new tests.
## New Features
- Added Immich Public Proxy service
- Add homepage service with dashboard contract implemented by all services
- Add scrutiny service.
- ZFS module now supports setting permissions
- Add landing page for mailserver and dashboard contract integration
## Bug Fixes
- Use configurable dataDir in arr stack
- Forgejo ensures ldap is setup when sso is configured
- Add nixpkgs patches on aarch64-linux too
- Self-signed certs are now idempotent
- Prometheus scrapes metrics at 15s interval instead of 1m
## Other Changes
- Arr stack declares ldap groups, declare ApiKeys and bypasses auth for readarr when sso is enabled
- Forgejo declares ldap group
# v0.7.3
## New Features
- Add [mailserver module](https://shb.skarabox.com/services-mailserver.html) integrating with [Simple NixOS Mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) and allowing full backup of an email provider.
- Bump nixpkgs from https://github.com/NixOS/nixpkgs/commit/5e2a59a5b1a82f89f2c7e598302a9cacebb72a67 to https://github.com/NixOS/nixpkgs/commit/bfc1b8a4574108ceef22f02bafcf6611380c100d. [Full diff](https://github.com/nixos/nixpkgs/compare/5e2a59a5b1a82f89f2c7e598302a9cacebb72a67...bfc1b8a4574108ceef22f02bafcf6611380c100d).
On top of minor changes, the most notable one was:
- Updated Jellyfin LDAP and SSO plugins and configuration. @Codys-Wright
## Bug Fixes
- Fix Restic and Authelia modules referencing systemd services without the `.service` suffix and leading to
# v0.7.2
## New Features
- Forgejo uses secrets contract for smtp password.
- Add [Firefly-iii](https://shb.skarabox.com/services-firefly-iii.html) service.
- Jellyfin can [install plugins declaratively](https://shb.skarabox.com/services-jellyfin.html#services-jellyfin-options-shb.jellyfin.plugins).
(Support is quite crude and WIP).
- Jellyfin configures LDAP and SSO fully declaratively, including installing necessary plugins.
- Nextcloud 32 is fully supported thanks to tests for version 31 and 32.
## Fixes
- Revert Authelia to continue using dots in systemd service names.
This caused issue with nginx name resolution.
## Other Changes
- Authelia uses non deprecated `smtp.address` option.
- Add documentation for Nginx block
- Now a user which is only member of the admin LDAP group of a service can login.
Before, some services required a user to be member of both the user and admin LDAP group.
This is ensured by regression tests going forward.
# v0.7.1
## New Features
- Add a Grafana dashboard showing SSL certificate renewal jobs
## Fixes
- Fix let's encrypt certificate renewal jobs by removing duplicated domain name.
Also adds an assertion to catch these kinds of errors.
## Other Changes
- Reduce number of late SSL renewal alert by merging all metrics corresponding to one CN.
# v0.7.0
## Breaking Changes
- Fix pkgs overrides not being passed to users of SelfHostBlocks.
This will require to update your flake to follow the example in the [Usage](https://shb.skarabox.com/usage.html) section.
## New Features
- Add a Grafana dashboard showing stats on backup jobs
and also an alert if a backup job did not run in the last 24 hours or never succeeded in the last 24 hours.
- Add SSO integration in Grafana.
- Add Paperless service.
## Fixes
- Allow to upload big files in Immich.
- Only enable php-fpm Prometheus exporter if Nextcloud is enabled.
## Other Changes
- Add recipe to setup DNS server with DNSSEC.
# v0.6.1
## New Features
- Implement backup and databasebackup contracts with BorgBackup block.
## Fixes
- Add back erroneously removed Prometheus collectors.
# v0.6.0
## Breaking Changes
- Removed Nextcloud 30, update to Nextcloud 31 then after to 32.
- Removed the `sops` module in the `default` NixOS module. Removed the `all` NixOS module.
## New Features
- Meilisearch configured with production environment and master key.
## Other Changes
- Only import hardcodedsecret module in tests.
- Better usage section in manual.
- Added new demo for minimal SelfHostBlocks setup, which is tested in CI.
- Format all files in repo and make sure they are formatted in CI.
# v0.5.1
## New Features
- Added Karakeep service with SSO integration.
- Add SelfHostBlocks' `lib` into `pkgs.lib.shb`. Integrates with [Skarabox](https://github.com/ibizaman/skarabox/blob/631ff5af0b5c850bb63a3b3df451df9707c0af4e/template/flake.nix#L42-L43) too.
## Other Changes
- Moved implementation guide under contributing section.
# v0.5.0
## Breaking Changes
- Modules in the `nixosModules` output field do not anymore have the `system` in their path.
`selfhostblocks.nixosModules.x86_64-linux.home-assistant` becomes `selfhostblocks.nixosModules.home-assistant`
like it always should have been.
## Fixes
- Added test case making sure a user belonging to a not authorized LDAP group cannot login.
Fixed Open WebUI module.
- Now importing a single module, like `selfhostblocks.nixosModules.home-assistant`, will
import all needed block modules at the same time.
## Other Changes
- Nextcloud module can now setup SSO integration without setting up LDAP integration.
# v0.4.4
## New Features
- Added Pinchflat service with SSO integration. Declarative user creation only supported through SSO integration.
- Added Immich service with SSO integration.
- Added Open WebUI service with SSO integration.
# v0.4.3
## New Features
- Allow user to change their SSO password in Authelia.
- Make Audiobookshelf SSO integration respect admin users.
## Fixes
- Fix permission on Nextcloud systemd service.
- Delete Forgejo backups correctly to avoid them piling up.
## Other Changes
- Add recipes section to the documentation.
# v0.4.2
## New Features
- The LLDAP and Authelia modules gain a debug mode where a mitmdump instance is added so all traffic is printed.
## Fixes
- By default, LLDAP module only enforces groups declaratively. Users that are not defined declaratively
are not anymore deleted by inadvertence.
- SSO integration with most services got fixed. A recent incompatible change in upstream Authelia broke most of them.
- Fixed PostgreSQL and Home Assistant modules after nixpkgs updates.
- Fixed Nextcloud module SSO integration with Authelia.
- Make Nextcloud SSO integration respect admin users.
# v0.4.1
## New Features
- LLDAP now manages users, groups, user attributes and group attributes declaratively.
- Individual modules are exposed in the flake output for each block and service.
- A mitmdump block is added that can be placed between two services and print all requests and responses.
- The SSO setup for Audiobookshelf is now a bit more declarative.
## Other Changes
- Forgejo got a new playwright test to check the LDAP integration.
- Some renaming options have been added retroactively for jellyfin and forgejo.
# v0.4.0
## Breaking Changes
- Rename ldap module to lldap as well as option name `shb.ldap` to `shb.lldap`.
## New Features
- Jellyfin service now waits for Jellyfin server to be fully available before starting.
- Add debug option for Jellyfin.
- Allow to choose port for Jellyfin.
- Make Jellyfin setup including initial admin user declarative.
## Fixes
- Fix Jellyfin redirect URI scheme after update.
## Other Changes
- Add documentation for LLDAP and Authelia block and link to it from other docs.
# v0.3.1
## Breaking Changes
- Default version of Nextcloud is now 30.
- Disable memories app on Nextcloud because it is broken.
## New Features
- Add patchNixpkgs function and pre-patched patchedNixpkgs output.
## Fixes
- Fix secrets passing to Nextcloud service after update.
## Other Changes
- Bump nixpkgs to https://github.com/NixOS/nixpkgs/commit/216207b1e58325f3590277d9102b45273afe9878
# v0.3.0
## New Features
- Add option to add extra args to hledger command.
## Breaking Changes
- Default version of Nextcloud is now 29.
## Fixes
- Home Assistant config gets correctly generated with secrets
even if LDAP integration is not enabled.
- Fix Jellyfin SSO plugin which was left badly configured
after a code refactoring.
## Other Changes
- Add a lot of playwright tests for services.
- Add service implementation manual page to document
how to integrate a service in SHB.
- Add `update-redirects` command to manage the `redirect.json` page.
- Add home-assistant manual.
# v0.2.10
## New Features
- Add `shb.forgejo.users` option to create users declaratively.
## Fixes
- Make Nextcloud create the external storage if it's a local storage
and the directory does not exist yet.
- Disable flow to change password on first login for admin Forgejo user.
This is not necessary since the password comes from some secret store.
## Breaking Changes
- Fix internal link for Home Assistant
which now points to the fqdn. This fixes Voice Assistant
onboarding. This is a breaking change if one relies on
reaching Home Assistant through the IP address but I
don't recommend that. It's much better to have a DNS
server running locally which redirects the fqdn to the
server running Home Assistant.
## Other Changes
- Refactor tests and add playwright tests for services.
# v0.2.9
## New Features
- Add Memories Nextcloud app declaratively configured.
- Add Recognize Nextcloud app declaratively configured.
# v0.2.8
## New Features
- Add dashboard for SSL certificates validity
and alert they did not renew on time.
## Fixes
- Only enable php-fpm exporter when php-fpm is enabled.
## Breaking Changes
- Remove upgrade script from postgres 13 to 14 and 14 to 15.
# v0.2.7
## New Features
- Add dashboard for Nextcloud with PHP-FPM exporter.
- Add voice option to Home-Assistant.
## User Facing Backwards Compatible Changes
- Add hostname and domain labels for scraped Prometheus metrics and Loki logs.
# v0.2.6
## New Features
- Add dashboard for deluge.
# v0.2.5
## Other Changes
- Fix more modules using backup contract.
# v0.2.4
## Other Changes
- Fix modules using backup contract.
# v0.2.3
## Breaking Changes
- Options `before_backup` and `after_backup` for backup contract have been renamed to
`beforeBackup` and `afterBackup`.
- All options using the backup and databasebackup contracts now use the new style.
## Other Changes
- Show how to pin Self Host Blocks flake input to a tag.
# v0.2.2
## User Facing Backwards Compatible Changes
- Fix: add implementation for `sops.nix` module.
## Other Changes
- Use VERSION when rendering manual too.
# v0.2.1
## User Facing Backwards Compatible Changes
- Add `sops.nix` module to `nixosModules.default`.
## Other Changes
- Auto-tagging of git repo when VERSION file gets updated.
- Add VERSION file to track version.
# v0.2.0
## New Features
- Backup:
- Add feature to backup databases with the database backup contract, implemented with `shb.restic.databases`.
## Breaking Changes
- Remove dependency on `sops-nix`.
- Rename `shb.nginx.autheliaProtect` to `shb.nginx.vhosts`. Indeed, the option allows to define a vhost with _optional_ Authelia protection but the former name made it look like Authelia protection was enforced. - Rename `shb.nginx.autheliaProtect` to `shb.nginx.vhosts`. Indeed, the option allows to define a vhost with _optional_ Authelia protection but the former name made it look like Authelia protection was enforced.
- Rename all `shb.arr.*.APIKey` to `shb.arr.*.ApiKey`. - Rename all `shb.arr.*.APIKey` to `shb.arr.*.ApiKey`.
- Remove `shb.vaultwarden.ldapEndpoint` option because it was not used in the implementation anyway. - Remove `shb.vaultwarden.ldapEndpoint` option because it was not used in the implementation anyway.
- Bump Nextcloud default version from 27 to 28. Add support for version 29. - Bump Nextcloud default version from 27 to 28. Add support for version 29.
- Deluge config breaks the authFile into an attrset of user to password file. Also deluge has tests now. - Deluge config breaks the authFile into an attrset of user to password file. Also deluge has tests now.
- Nextcloud now configures the LDAP app to use the `user_id` from LLDAP as the user ID used in Nextcloud. This makes all source of user - internal, LDAP and SSO - agree on the user ID. - Nextcloud now configures the LDAP app to use the `user_id` from LLDAP as the user ID used in Nextcloud. This makes all source of user - internal, LDAP and SSO - agree on the user ID.
- Authelia options changed:
- `shb.authelia.oidcClients.id` -> `shb.authelia.oidcClients.client_id`
- `shb.authelia.oidcClients.description` -> `shb.authelia.oidcClients.client_name`
- `shb.authelia.oidcClients.secret` -> `shb.authelia.oidcClients.client_secret`
- `shb.authelia.ldapEndpoint` -> `shb.authelia.ldapHostname` and `shb.authelia.ldapPort`
- `shb.authelia.jwtSecretFile` -> `shb.authelia.jwtSecret.result.path`
- `shb.authelia.ldapAdminPasswordFile` -> `shb.authelia.ldapAdminPassword.result.path`
- `shb.authelia.sessionSecretFile` -> `shb.authelia.sessionSecret.result.path`
- `shb.authelia.storageEncryptionKeyFile` -> `shb.authelia.storageEncryptionKey.result.path`
- `shb.authelia.identityProvidersOIDCIssuerPrivateKeyFile` -> `shb.authelia.identityProvidersOIDCIssuerPrivateKey.result.path`
- `shb.authelia.smtp.passwordFile` -> `shb.authelia.smtp.password.result.path`
- Make Nextcloud automatically disable maintenance mode upon service restart.
- `shb.ldap.ldapUserPasswordFile` -> `shb.ldap.ldapUserPassword.result.path`
- `shb.ldap.jwtSecretFile` -> `shb.ldap.jwtSecret.result.path`
- Jellyfin changes:
- `shb.jellyfin.ldap.passwordFile` -> `shb.jellyfin.ldap.adminPassword.result.path`.
- `shb.jellyfin.sso.secretFile` -> `shb.jellyfin.ldap.sharedSecret.result.path`.
- + `shb.jellyfin.ldap.sharedSecretForAuthelia`.
- Forgejo changes:
- `shb.forgejo.ldap.adminPasswordFile` -> `shb.forgejo.ldap.adminPassword.result.path`.
- `shb.forgejo.sso.secretFile` -> `shb.forgejo.ldap.sharedSecret.result.path`.
- `shb.forgejo.sso.secretFileForAuthelia` -> `shb.forgejo.ldap.sharedSecretForAuthelia.result.path`.
- `shb.forgejo.adminPasswordFile` -> `shb.forgejo.adminPassword.result.path`.
- `shb.forgejo.databasePasswordFile` -> `shb.forgejo.databasePassword.result.path`.
- Backup:
- `shb.restic.instances` options has been split between `shb.restic.instances.request` and `shb.restic.instances.settings`, matching better with contracts.
- Use of secret contract everywhere.
## User Facing Backwards Compatible Changes ## User Facing Backwards Compatible Changes
- Add mount contract.
- Export torrent metrics.
- Bump chunkSize in Nextcloud to boost performance.
- Fix home-assistant onboarding file generation. Added new VM test. - Fix home-assistant onboarding file generation. Added new VM test.
- OIDC and SMTP config are now optional in Vaultwarden. Added new VM test. - OIDC and SMTP config are now optional in Vaultwarden. Added new VM test.
- Add default OIDC config for Authelia. This way, Authelia can start even with no config or only forward auth configs. - Add default OIDC config for Authelia. This way, Authelia can start even with no config or only forward auth configs.

476
README.md
View file

@ -1,86 +1,35 @@
![GitHub Release](https://img.shields.io/github/v/release/ibizaman/selfhostblocks)
![GitHub commits since latest release (branch)](https://img.shields.io/github/commits-since/ibizaman/selfhostblocks/latest/main)
![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/w/ibizaman/selfhostblocks/main)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr-raw/ibizaman/selfhostblocks)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr-closed-raw/ibizaman/selfhostblocks?label=closed)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-raw/ibizaman/selfhostblocks)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-closed-raw/ibizaman/selfhostblocks?label=closed)
[![Documentation](https://github.com/ibizaman/selfhostblocks/actions/workflows/pages.yml/badge.svg)](https://github.com/ibizaman/selfhostblocks/actions/workflows/pages.yml)
[![Tests](https://github.com/ibizaman/selfhostblocks/actions/workflows/build.yaml/badge.svg)](https://github.com/ibizaman/selfhostblocks/actions/workflows/build.yaml)
[![Demo](https://github.com/ibizaman/selfhostblocks/actions/workflows/demo.yml/badge.svg)](https://github.com/ibizaman/selfhostblocks/actions/workflows/demo.yml)
![Matrix](https://img.shields.io/matrix/selfhostblocks%3Amatrix.org)
<hr />
# Self Host Blocks # Self Host Blocks
SelfHostBlocks is: *Building blocks for self-hosting with battery included.*
- Your escape from the cloud, for privacy and data sovereignty enthusiast. [Why?](#why-self-hosting) [![Documentation](https://github.com/ibizaman/selfhostblocks/actions/workflows/pages.yml/badge.svg)](https://github.com/ibizaman/selfhostblocks/actions/workflows/pages.yml)
- A groupware to self-host [all your data](#services): documents, pictures, calendars, contacts, etc. [![Tests](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fgarnix.io%2Fapi%2Fbadges%2Fibizaman%2Fselfhostblocks%3Fbranch%3Dmain)](https://garnix.io) (using Garnix)
- An opinionated NixOS server management OS for a [safe self-hosting experience](#features).
- A NixOS distribution making sure all services build and work correctly thanks to NixOS VM tests.
- A collection of NixOS modules standardizing options so configuring services [look the same](#unified-interfaces).
- A testing ground for [contracts](#contracts) which intents to make nixpkgs modules more modular.
- [Upstreaming][] as much as possible.
[upstreaming]: https://github.com/pulls?page=1&q=created%3A%3E2023-06-01+is%3Apr+author%3Aibizaman+archived%3Afalse+-repo%3Aibizaman%2Fselfhostblocks+-repo%3Aibizaman%2Fskarabox SHB's (Self Host Blocks) is yet another server management tool whose goal is to provide better
building blocks for self-hosting. Indeed, SHB provides opinionated [building
blocks](#available-blocks) fitting together to self-host any service you'd want. Some [common
services](#provided-services) are provided out of the box.
## Why Self-Hosting SHB's goal is to make these building blocks plug-and-play. To achieve this, SHB pioneers
[contracts](https://shb.skarabox.com/contracts.html) which allows you, the final user, to be more in
control of which pieces go where. The promise here is to let you choose, for example, any reverse
proxy you want or any database you want, without requiring work from maintainers of the services you
want to self host.
It is obvious by now that To achieve all this, SHB is using the full power of NixOS modules and NixOS VM tests. Indeed, each
a deep dependency on proprietary service providers - "the cloud" - building block and each service is a NixOS module using modules defined in
is a significant liability. [Nixpkgs](https://github.com/NixOS/nixpkgs/) and they are tested using full VMs on every commit.
One aspect often talked about is privacy
which is inherently not guaranteed when using a proprietary service
and is a valid concern.
A more punishing issue is having your account closed or locked
without prior warning
When that happens,
you get an instantaneous sinking feeling in your stomach
at the realization you lost access to your data,
possibly without recourse.
Hosting services yourself is the obvious alternative
to alleviate those concerns
but it tends to require a lot of technical skills and time.
SelfHostBlocks (together with its sibling project [Skarabox][])
aims to lower the bar to self-hosting,
and provides an opinionated server management system based on NixOS modules
embedding best practices.
Contrary to other server management projects,
its main focus is ease of long term maintenance
before ease of installation.
To achieve this, it provides building blocks to setup services.
Some are already provided out of the box,
and customizing or adding additional ones is done easily.
The building blocks fit nicely together thanks to [contracts](#contracts)
which SelfHostBlocks sets out to introduce into nixpkgs.
This will increase modularity, code reuse
and empower end users to assemble components
that fit together to build their server.
## TOC ## TOC
<!--toc:start--> <!--toc:start-->
- [Usage](#usage) - [Usage](#usage)
- [At a Glance](#at-a-glance) - [Manual](#manual)
- [Existing Installation](#existing-installation)
- [Installation From Scratch](#installation-from-scratch)
- [Features](#features)
- [Services](#services)
- [Blocks](#blocks)
- [Unified Interfaces](#unified-interfaces)
- [Contracts](#contracts)
- [Interfacing With Other OSes](#interfacing-with-other-oses)
- [Sitting on the Shoulders of a Giant](#sitting-on-the-shoulders-of-a-giant)
- [Automatic Updates](#automatic-updates)
- [Demos](#demos)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Available Blocks](#available-blocks)
- [Provided Services](#provided-services)
- [Demos](#demos)
- [Community](#community) - [Community](#community)
- [Funding](#funding)
- [License](#license) - [License](#license)
<!--toc:end--> <!--toc:end-->
@ -90,350 +39,121 @@ that fit together to build their server.
> production server, this is really just a one person effort for now and there are most certainly > production server, this is really just a one person effort for now and there are most certainly
> bugs that I didn't discover yet. > bugs that I didn't discover yet.
To get started using SelfHostBlocks, the following snippet is enough: Self Host Blocks is available as a flake. To use it in your project, add the following flake input:
```nix ```nix
{
inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks"; inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks";
outputs = { selfhostblocks, ... }: let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
in
nixosConfigurations = {
myserver = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
./configuration.nix
];
};
};
}
``` ```
SelfHostBlocks provides its own patched nixpkgs, so you are required to use it This is not quite enough though and more information is provided in [the
otherwise evaluation can quickly break. manual](https://shb.skarabox.com/usage.html).
[The usage section](https://shb.skarabox.com/usage.html) of the manual has
more details and goes over how to deploy with [Colmena][], [nixos-rebuild][] and [deploy-rs][]
and also how to handle secrets management with [SOPS][].
[Colmena]: https://shb.skarabox.com/usage.html#usage-example-colmena - You are new to self hosting and want pre-configured services to deploy easily. Look at the
[nixos-rebuild]: https://shb.skarabox.com/usage.html#usage-example-nixosrebuild [services section](https://shb.skarabox.com/services.html).
[deploy-rs]: https://shb.skarabox.com/usage.html#usage-example-deployrs - You are a seasoned self-hoster but want to enhance some services you deploy already. Go to the
[SOPS]: https://shb.skarabox.com/usage.html#usage-secrets [blocks section](https://shb.skarabox.com/blocks.html).
- You are a user of Self Host Blocks but would like to use your own implementation for a block. Go
to the [contracts section](https://shb.skarabox.com/contracts.html).
Then, to actually configure services, you can choose which one interests you in Head over to the [matrix channel](https://matrix.to/#/#selfhostblocks:matrix.org) for any remaining
the [services section](https://shb.skarabox.com/services.html) of the manual. question, or just to say hi :)
The [recipes section](https://shb.skarabox.com/recipes.html) of the manual shows some other common use cases. ## Why yet another self hosting tool?
Head over to the [matrix channel](https://matrix.to/#/#selfhostblocks:matrix.org) By using Self Host Blocks, you get all the benefits of NixOS which are, for self hosted applications
for any remaining question, or just to say hi :) specifically:
### Installation From Scratch
I do recommend for this my sibling project [Skarabox][]
which bootstraps a new server and sets up a few tools:
- Create a bootable ISO, installable on an USB key.
- Handles one or two (in raid 1) SSDs for root partition.
- Handles two (in raid 1) or more hard drives for data partition.
- [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) to install NixOS headlessly.
- [disko](https://github.com/nix-community/disko) to format the drives using native ZFS encryption with remote unlocking through ssh.
- [sops-nix](https://github.com/Mic92/sops-nix) to handle secrets.
- [deploy-rs](https://github.com/serokell/deploy-rs) to deploy updates.
[Skarabox]: https://github.com/ibizaman/skarabox
## Features
SelfHostBlocks provides building blocks that take care of common self-hosting needs:
- Backup for all services.
- Automatic creation of ZFS datasets per service.
- LDAP and SSO integration for most services.
- Monitoring with Grafana and Prometheus stack with provided dashboards and integration with Scrutiny.
- Automatic reverse proxy and certificate management for HTTPS.
- VPN and proxy tunneling services.
Great care is taken to make the proposed stack robust.
This translates into a test suite comprised of automated NixOS VM tests
which includes playwright tests to verify some important workflow
like logging in.
This test suite also serves as a guaranty that all services provided by SelfHostBlocks
all evaluate, build and work correctly together. It works similarly as a distribution but here it's all [automated](#automatic-updates).
Also, the stack fits together nicely thanks to [contracts](#contracts).
### Services
[Provided services](https://shb.skarabox.com/services.html) are:
- Nextcloud
- Audiobookshelf
- Deluge + *arr stack
- Simple NixOS Mailserver
- Firefly-iii
- Forgejo
- Grocy
- Hledger
- Home-Assistant
- Jellyfin
- Karakeep
- Open WebUI
- Pinchflat
- Vaultwarden
Like explained above, those services all benefit from
out of the box backup,
LDAP and SSO integration,
monitoring with Grafana,
reverse proxy and certificate management
and VPN integration for the *arr suite.
Some services do not have an entry yet in the manual.
To know options for those, the only way for now
is to go to the [All Options][] section of the manual.
[All Options]: https://shb.skarabox.com/options.html
### Blocks
The services above rely on the following [common blocks][]
which altogether provides a solid foundation for self-hosting services:
- Authelia
- BorgBackup
- Davfs
- LDAP
- Monitoring (Grafana - Prometheus - Loki stack + Scrutiny)
- Nginx
- PostgreSQL
- Restic
- Sops
- SSL
- Tinyproxy
- VPN
- ZFS
Those blocks can be used with services
not provided by SelfHostBlocks as shown [in the manual][common blocks].
[common blocks]: https://shb.skarabox.com/blocks.html
The manual also provides documentation for each individual blocks.
### Unified Interfaces
Thanks to the blocks,
SelfHostBlocks provides an unified configuration interface
for the services it provides.
Compare the configuration for Nextcloud and Forgejo.
The following snippets focus on similitudes and assume the relevant blocks - like secrets - are configured off-screen.
It also does not show specific options for each service.
These are still complete snippets that configure HTTPS,
subdomain serving the service, LDAP and SSO integration.
```nix
shb.nextcloud = {
enable = true;
subdomain = "nextcloud";
domain = "example.com";
ssl = config.shb.certs.certs.letsencrypt.${domain};
apps.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
};
apps.sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."nextcloud/sso/secretForAuthelia".result;
};
};
```
```nix
shb.forgejo = {
enable = true;
subdomain = "forgejo";
domain = "example.com";
ssl = config.shb.certs.certs.letsencrypt.${domain};
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
};
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secret.result = config.shb.sops.secret."forgejo/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."forgejo/sso/secretForAuthelia".result;
};
};
```
As you can see, they are pretty similar!
This makes setting up a new service pretty easy and intuitive.
SelfHostBlocks provides an ever growing list of [services](#services)
that are configured in the same way.
### Contracts
To make building blocks that fit nicely together,
SelfHostBlocks pioneers [contracts][] which allows you, the final user,
to be more in control of which piece goes where.
This lets you choose, for example,
any reverse proxy you want or any database you want,
without requiring work from maintainers of the services you want to self host.
An [RFC][] exists to upstream this concept into `nixpkgs`.
The [manual][contracts] also provides an explanation of the why and how of contracts.
Also, two videos exist of me presenting the topic,
the first at [NixCon North America in spring of 2024][NixConNA2024]
and the second at [NixCon in Berlin in fall of 2024][NixConBerlin2024].
[contracts]: https://shb.skarabox.com/contracts.html
[RFC]: https://github.com/NixOS/rfcs/pull/189
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
### Interfacing With Other OSes
Thanks to [contracts](#contracts), one can interface NixOS
with systems on other OSes.
The [RFC][] explains how that works.
### Sitting on the Shoulders of a Giant
By using SelfHostBlocks, you get all the benefits of NixOS
which are, for self hosted applications specifically:
- declarative configuration; - declarative configuration;
- atomic configuration rollbacks; - atomic configuration rollbacks;
- real programming language to define configurations; - real programming language to define configurations;
- create your own higher level abstractions on top of SelfHostBlocks; - user-defined abstractions (create your own functions or NixOS modules on top of SHB!);
- integration with the rest of nixpkgs; - integration with the rest of nixpkgs;
- much fewer "works on my machine" type of issues. - much fewer "works on my machine" type of issues.
### Automatic Updates In no particular order, here are some aspects of SHB which I find interesting and differentiates it
from other server management projects:
SelfHostBlocks follows nixpkgs unstable branch closely. - SHB intends to be a library, not a framework. You can either go all in and use SHB provided
There is a GitHub action running every couple of days that updates services directly or use just one block in your existing infrastructure.
the `nixpkgs` input in the root `flakes.nix`, - SHB introduces [contracts](https://shb.skarabox.com/contracts.html) to allow you to swap
runs the tests and merges the PR automatically implementation for each self-hosting need. For example, you should be able to use the reverse
if the tests pass. proxy you want without modifying any services depending on it.
- SHB contracts also allows you to use your own custom implementation instead of the provided one,
as long as it follows the contract and passes the tests.
- SHB provides at least one implementation for each contract like backups, SSL certificates, reverse
proxy, VPN, etc. Those are called blocks here and are documented in [the
manual](https://shb.skarabox.com/blocks.html).
- SHB provides several services out of the box fully using the blocks provided. Those can also be
found in [the manual](https://shb.skarabox.com/services.html).
- SHB follows nixpkgs unstable branch closely. There is a GitHub action running daily that updates
the `nixpkgs` input in the root `flakes.nix`, runs the tests and merges a PR with the new input if
the tests pass.
A release is then made every few commits, ## Manual
whenever deemed sensible.
On your side, to update I recommend pinning to a release
with the following command,
replacing the RELEASE with the one you want:
```bash The manual can be found at [shb.skarabox.com](https://shb.skarabox.com/).
RELEASE=0.2.4
nix flake update \
--override-input selfhostblocks github:ibizaman/selfhostblocks/$RELEASE \
selfhostblocks
```
### Demos Work is in progress to document everything in the manual but I'm not there yet. For what's not yet
documented, unfortunately the source code is the best place to read about them.
Demos that start and deploy a service [Here](./modules/services) for services and [here](./modules/blocks) for blocks.
on a Virtual Machine on your computer are located
under the [demo](./demo/) folder.
These show the onboarding experience you would get
if you deployed one of the services on your own server.
## Roadmap ## Roadmap
Currently, the Nextcloud and Vaultwarden services Currently, the Nextcloud service and SSL block are the most advanced and most documented.
and the SSL and backup blocks
are the most advanced and most documented.
Documenting all services and blocks will be done Documenting all services and blocks will be done as I make all blocks and services use the
as I make all blocks and services use the contracts. contracts.
Upstreaming changes is also on the roadmap. Upstreaming changes is also on the roadmap.
Check the [issues][] and the [milestones]() to see planned work. Check [the issues](https://github.com/ibizaman/selfhostblocks/issues) to see planned works. Feel
Feel free to add more or to contribute! free to add more!
[issues]: (https://github.com/ibizaman/selfhostblocks/issues) That being said, I am personally using all the blocks and services in this project, so they do work
[milestones]: https://github.com/ibizaman/selfhostblocks/milestones to some extent.
All blocks and services have NixOS tests. ## Available Blocks
Also, I am personally using all the blocks and services in this project, so they do work to some extent.
- [`authelia.nix`](./modules/blocks/authelia.nix) for Single Sign On.
- [`backup.nix`](./modules/blocks/backup.nix).
- [`ldap.nix`](./modules/blocks/ldap.nix) for user management.
- [`monitoring.nix`](./modules/blocks/monitoring.nix) for dashboards, logs and alerts.
- [`nginx.nix`](./modules/blocks/nginx.nix) for reverse proxy with SSL termination.
- [`postgresql.nix`](./modules/blocks/postgresql.nix) for database setup.
- [`ssl.nix`](./modules/blocks/ssl.nix) for maintaining self-signed SSL certificates or certificates provided by Let's Encrypt.
- [`tinyproxy.nix`](./modules/blocks/tinyproxy.nix) to forward traffic to a VPN tunnel.
- [`vpn.nix`](./modules/blocks/vpn.nix) to setup a VPN tunnel.
## Provided Services
- [`arr.nix`](./modules/services/arr.nix) for finding media https://wiki.servarr.com/.
- [`deluge.nix`](./modules/services/deluge.nix) for downloading linux isos https://deluge-torrent.org/.
- [`hledger.nix`](./modules/services/hledger.nix) for managing finances https://hledger.org/.
- [`home-assistant.nix`](./modules/services/home-assistant.nix) for private IoT https://www.home-assistant.io/.
- [`jellyfin.nix`](./modules/services/jellyfin.nix) for watching media https://jellyfin.org/.
- [Nextcloud Server](https://shb.skarabox.com/services-nextcloud.html) for private documents, contacts, calendar, etc https://nextcloud.com.
- [`vaultwarden.nix`](./modules/services/vaultwarden.nix) for passwords https://github.com/dani-garcia/vaultwarden.
- [`audiobookshelf.nix`](./modules/services/audiobookshelf.nix) for hosting podcasts and audio books https://www.audiobookshelf.org/.
## Demos
Demos that start and deploy a service on a Virtual Machine on your computer are located under the
[demo](./demo/) folder. These show the onboarding experience you would get if you deployed one of
the services on your own server.
## Community ## Community
This project has been the main focus All issues and PRs are welcome. For PRs, if they are substantial changes, please open an issue to
of my (non work) life for the past 3 year now discuss the details first. More details in [here](https://shb.skarabox.com/contributing.html).
and I intend to continue working on this for a long time.
All issues and PRs are welcome: Come hang out in the [Matrix channel](https://matrix.to/#/%23selfhostblocks%3Amatrix.org). :)
- Use this project. Something does not make sense? Something's not working? One aspect that's close to my heart is I intent to make SHB the lightest layer on top of nixpkgs as
- Documentation. Something is not clear?
- New services. Have one of your preferred service not integrated yet?
- Better patterns. See something weird in the code?
For PRs, if they are substantial changes, please open an issue to
discuss the details first. More details in [the contributing section](https://shb.skarabox.com/contributing.html)
of the manual.
Issues that are being worked on are labeled with the [in progress][] label.
Before starting work on those, you might want to talk about it in the issue tracker
or in the [matrix][] channel.
The prioritized issues are those belonging to the [next milestone][milestone].
Those issues are not set in stone and I'd be very happy to solve
an issue an user has before scratching my own itch.
[in progress]: https://github.com/ibizaman/selfhostblocks/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22in%20progress%22
[matrix]: https://matrix.to/#/%23selfhostblocks%3Amatrix.org
[milestone]: https://github.com/ibizaman/selfhostblocks/milestones
One aspect that's close to my heart is I intent to make SelfHostBlocks the lightest layer on top of nixpkgs as
possible. I want to upstream as much as possible. I will still take some time to experiment here but possible. I want to upstream as much as possible. I will still take some time to experiment here but
when I'm satisfied with how things look, I'll upstream changes. when I'm satisfied with how things look, I'll upstream changes.
## Funding
I was lucky to [obtain a grant][nlnet] from NlNet which is an European fund,
under [NGI Zero Core][NGI0],
to work on this project.
This also funds the contracts RFC.
Go apply for a grant too!
[nlnet]: https://nlnet.nl/project/SelfHostBlocks
[NGI0]: https://nlnet.nl/core/
<p>
<img alt="NlNet logo" src="https://nlnet.nl/logo/banner.svg" width="200" />
<img alt="NGI Zero Core logo" src="https://nlnet.nl/image/logos/NGI0Core_tag.svg" width="200" />
</p>
## License ## License
I'm following the [Nextcloud](https://github.com/nextcloud/server) license which is AGPLv3. I'm following the [Nextcloud](https://github.com/nextcloud/server) license which is AGPLv3. See
See [this article](https://www.fsf.org/bulletin/2021/fall/the-fundamentals-of-the-agplv3) from the FSF that explains what this license adds to the GPL one. [this article](https://www.fsf.org/bulletin/2021/fall/the-fundamentals-of-the-agplv3) from the FSF that explains what this license adds to the GPL
one.

View file

@ -1 +0,0 @@
0.9.0

View file

@ -60,7 +60,7 @@ chmod 600 sshkey
This is only needed because git mangles with the permissions. You will not even see this change in This is only needed because git mangles with the permissions. You will not even see this change in
`git status`. `git status`.
### Deploy with Colmena {#demo-homeassistant-deploy-colmena} ### Deploy with Colmena {#demo-homeassitant-deploy-colmena}
If you deploy with Colmena, you must first build the VM and start it: If you deploy with Colmena, you must first build the VM and start it:
@ -177,7 +177,7 @@ user and password you just created above.
### Files {#demo-homeassistant-files} ### Files {#demo-homeassistant-files}
- [`flake.nix`](./flake.nix): nix entry point, defines one target host for - [`flake.nix`](./flake.nix): nix entry point, defines one target host for
[colmena](https://colmena.cli.rs) to deploy to as well as the selfhostblocks' config for [colmena](https://colmena.cli.rs) to deploy to as well as the selfhostblock's config for
setting up the home assistant server paired with the LDAP server. setting up the home assistant server paired with the LDAP server.
- [`configuration.nix`](./configuration.nix): defines all configuration required for colmena - [`configuration.nix`](./configuration.nix): defines all configuration required for colmena
to deploy to the VM. The file has comments if you're interested. to deploy to the VM. The file has comments if you're interested.

View file

@ -5,8 +5,8 @@ let
targetPort = 2222; targetPort = 2222;
in in
{ {
imports = [ imports =
# Include the results of the hardware scan. [ # Include the results of the hardware scan.
./hardware-configuration.nix ./hardware-configuration.nix
]; ];
@ -26,10 +26,7 @@ in
# Options above are needed to deploy in a VM. # Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ nix.settings.experimental-features = [ "nix-command" "flakes" ];
"nix-command"
"flakes"
];
# We need to create the user we will deploy with. # We need to create the user we will deploy with.
users.users.${targetUser} = { users.users.${targetUser} = {
@ -44,11 +41,9 @@ in
# The user we're deploying with must be able to run sudo without password. # The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [ security.sudo.extraRules = [
{ { users = [ targetUser ];
users = [ targetUser ];
commands = [ commands = [
{ { command = "ALL";
command = "ALL";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
]; ];
@ -64,9 +59,7 @@ in
services.openssh = { services.openssh = {
enable = true; enable = true;
ports = [ targetPort ]; ports = [ targetPort ];
settings = { permitRootLogin = "no";
PermitRootLogin = "no"; passwordAuthentication = false;
PasswordAuthentication = false;
};
}; };
} }

View file

@ -0,0 +1,164 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-flake-tests": {
"locked": {
"lastModified": 1677844186,
"narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
"owner": "antifuchs",
"repo": "nix-flake-tests",
"rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
"type": "github"
},
"original": {
"owner": "antifuchs",
"repo": "nix-flake-tests",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1709150264,
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1708819810,
"narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1708751719,
"narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nmdsrc": {
"flake": false,
"locked": {
"lastModified": 1705050560,
"narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"ref": "refs/heads/master",
"rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"revCount": 66,
"type": "git",
"url": "https://git.sr.ht/~rycee/nmd"
},
"original": {
"type": "git",
"url": "https://git.sr.ht/~rycee/nmd"
}
},
"root": {
"inputs": {
"selfhostblocks": "selfhostblocks"
}
},
"selfhostblocks": {
"inputs": {
"flake-utils": "flake-utils",
"nix-flake-tests": "nix-flake-tests",
"nixpkgs": "nixpkgs",
"nmdsrc": "nmdsrc",
"sops-nix": "sops-nix"
},
"locked": {
"lastModified": 1709267447,
"narHash": "sha256-5Q467FhpS18L/+5iB3wsWaR9tBqdzNt0fpdkZJNqNxc=",
"owner": "ibizaman",
"repo": "selfhostblocks",
"rev": "fa206d0e1515fb0e49393e7ada6d7e5c6ec1df58",
"type": "github"
},
"original": {
"owner": "ibizaman",
"repo": "selfhostblocks",
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1708987867,
"narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -3,65 +3,53 @@
inputs = { inputs = {
selfhostblocks.url = "github:ibizaman/selfhostblocks"; selfhostblocks.url = "github:ibizaman/selfhostblocks";
sops-nix.url = "github:Mic92/sops-nix";
}; };
outputs = outputs = inputs@{ self, selfhostblocks, ... }:
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let let
system = "x86_64-linux"; basic = { config, ... }: {
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
basic =
{ config, ... }:
{
imports = [ imports = [
./configuration.nix ./configuration.nix
selfhostblocks.nixosModules.authelia selfhostblocks.inputs.sops-nix.nixosModules.default
selfhostblocks.nixosModules.home-assistant selfhostblocks.nixosModules.x86_64-linux.default
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
]; ];
sops.defaultSopsFile = ./secrets.yaml;
shb.home-assistant = { shb.home-assistant = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "ha"; subdomain = "ha";
config = { config = {
name = "SHB Home Assistant"; name = "SHB Home Assistant";
country.source = config.shb.sops.secret."home-assistant/country".result.path; country.source = config.sops.secrets."home-assistant/country".path;
latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path; latitude.source = config.sops.secrets."home-assistant/latitude".path;
longitude.source = config.shb.sops.secret."home-assistant/longitude".result.path; longitude.source = config.sops.secrets."home-assistant/longitude".path;
time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path; time_zone.source = config.sops.secrets."home-assistant/time_zone".path;
unit_system = "metric"; unit_system = "metric";
}; };
}; };
shb.sops.secret."home-assistant/country".request = { sops.secrets."home-assistant/country" = {
sopsFile = ./secrets.yaml;
mode = "0440"; mode = "0440";
owner = "hass"; owner = "hass";
group = "hass"; group = "hass";
restartUnits = [ "home-assistant.service" ]; restartUnits = [ "home-assistant.service" ];
}; };
shb.sops.secret."home-assistant/latitude".request = { sops.secrets."home-assistant/latitude" = {
sopsFile = ./secrets.yaml;
mode = "0440"; mode = "0440";
owner = "hass"; owner = "hass";
group = "hass"; group = "hass";
restartUnits = [ "home-assistant.service" ]; restartUnits = [ "home-assistant.service" ];
}; };
shb.sops.secret."home-assistant/longitude".request = { sops.secrets."home-assistant/longitude" = {
sopsFile = ./secrets.yaml;
mode = "0440"; mode = "0440";
owner = "hass"; owner = "hass";
group = "hass"; group = "hass";
restartUnits = [ "home-assistant.service" ]; restartUnits = [ "home-assistant.service" ];
}; };
shb.sops.secret."home-assistant/time_zone".request = { sops.secrets."home-assistant/time_zone" = {
sopsFile = ./secrets.yaml;
mode = "0440"; mode = "0440";
owner = "hass"; owner = "hass";
group = "hass"; group = "hass";
@ -73,26 +61,36 @@
]; ];
}; };
ldap = ldap = { config, ... }: {
{ config, ... }: shb.ldap = {
{
shb.lldap = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "ldap"; subdomain = "ldap";
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; ldapUserPasswordFile = config.sops.secrets."lldap/user_password".path;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; jwtSecretFile = config.sops.secrets."lldap/jwt_secret".path;
};
sops.secrets."lldap/user_password" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
sops.secrets."lldap/jwt_secret" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
}; };
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.home-assistant.ldap = { shb.home-assistant.ldap = {
enable = true; enable = true;
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.lldap.webUIListenPort; port = config.shb.ldap.webUIListenPort;
userGroup = "homeassistant_user"; userGroup = "homeassistant_user";
}; };
}; };
@ -104,7 +102,7 @@
in in
{ {
nixosConfigurations = { nixosConfigurations = {
basic = nixpkgs'.nixosSystem { basic = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
basic basic
@ -112,7 +110,7 @@
]; ];
}; };
ldap = nixpkgs'.nixosSystem { ldap = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
basic basic
@ -124,15 +122,13 @@
colmena = { colmena = {
meta = { meta = {
nixpkgs = import nixpkgs' { nixpkgs = import selfhostblocks.inputs.nixpkgs {
system = "x86_64-linux"; system = "x86_64-linux";
}; };
specialArgs = inputs; specialArgs = inputs;
}; };
basic = basic = { config, ... }: {
{ config, ... }:
{
imports = [ imports = [
basic basic
]; ];
@ -145,9 +141,7 @@
}; };
}; };
ldap = ldap = { config, ... }: {
{ config, ... }:
{
imports = [ imports = [
basic basic
ldap ldap

View file

@ -3,63 +3,50 @@
# Do not modify this file! It was generated by nixos-generate-config # Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes # and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead. # to /etc/nixos/configuration.nix instead.
{ { config, lib, pkgs, modulesPath, ... }:
config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = [ imports =
(modulesPath + "/profiles/qemu-guest.nix") [ (modulesPath + "/profiles/qemu-guest.nix")
]; ];
boot.initrd.availableKernelModules = [ boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
"ata_piix"
"uhci_hcd"
"virtio_pci"
"floppy"
"sr_mod"
"virtio_blk"
];
boot.initrd.kernelModules = [ ]; boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ]; boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
fileSystems."/" = { fileSystems."/" =
device = "/dev/vda"; { device = "/dev/vda";
fsType = "ext4"; fsType = "ext4";
}; };
fileSystems."/nix/.ro-store" = { fileSystems."/nix/.ro-store" =
device = "nix-store"; { device = "nix-store";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/nix/.rw-store" = { fileSystems."/nix/.rw-store" =
device = "tmpfs"; { device = "tmpfs";
fsType = "tmpfs"; fsType = "tmpfs";
}; };
fileSystems."/tmp/shared" = { fileSystems."/tmp/shared" =
device = "shared"; { device = "shared";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/tmp/xchg" = { fileSystems."/tmp/xchg" =
device = "xchg"; { device = "xchg";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/nix/store" = { fileSystems."/nix/store" =
device = "overlay"; { device = "overlay";
fsType = "overlay"; fsType = "overlay";
}; };
fileSystems."/boot" = { fileSystems."/boot" =
device = "/dev/vdb2"; { device = "/dev/vdb2";
fsType = "vfat"; fsType = "vfat";
}; };

View file

@ -1,223 +0,0 @@
{
description = "Minimal example to setup SelfHostBlocks";
inputs = {
selfhostblocks.url = "github:ibizaman/selfhostblocks";
sops-nix = {
url = "github:Mic92/sops-nix";
};
};
outputs =
{
self,
selfhostblocks,
sops-nix,
}:
{
nixosConfigurations =
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
# This module makes the assertions happy and the build succeed.
# This is of course wrong and will not work on any real system.
filesystemModule = {
fileSystems."/" = {
device = "/dev/null";
fsType = "none";
};
boot.loader.grub.devices = [ "/dev/null" ];
};
in
{
# Test with:
# nix build .#nixosConfigurations.minimal.config.system.build.toplevel
minimal = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
filesystemModule
# This modules showcases the use of SHB's lib.
(
{
config,
lib,
shb,
...
}:
{
options.myOption = lib.mkOption {
# Using provided nixosSystem directly.
# SHB's lib is available under `shb` thanks to the overlay.
type = shb.secretFileType;
};
config = {
myOption.source = "/a/path";
# Use the option.
environment.etc.myOption.text = config.myOption.source;
};
}
)
];
};
# Test with:
# nix build .#nixosConfigurations.sops.config.system.build.toplevel
# nix eval .#nixosConfigurations.sops.config.myOption
sops = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
selfhostblocks.nixosModules.sops
sops-nix.nixosModules.default
filesystemModule
# This modules showcases the use of SHB's lib.
(
{
config,
lib,
shb,
...
}:
{
options.myOption = lib.mkOption {
# Using provided nixosSystem directly.
# SHB's lib is available under `shb` thanks to the overlay.
type = shb.secretFileType;
};
config = {
myOption.source = "/a/path";
# Use the option.
environment.etc.myOption.text = config.myOption.source;
};
}
)
];
};
# This example shows how to import the nixosSystem patches to nixpkgs manually.
#
# Test with:
# nix build .#nixosConfigurations.lowlevel.config.system.build.toplevel
# nix eval .#nixosConfigurations.lowlevel.config.myOption
lowlevel =
let
# We must import nixosSystem directly from the patched nixpkgs
# otherwise we do not get the patches.
nixosSystem' = import "${nixpkgs'}/nixos/lib/eval-config.nix";
in
nixosSystem' {
inherit system;
modules = [
selfhostblocks.nixosModules.default
filesystemModule
# This modules showcases the use of SHB's lib.
(
{
config,
lib,
shb,
...
}:
{
options.myOption = lib.mkOption {
# Using provided nixosSystem directly.
# SHB's lib is available under `shb` thanks to the overlay.
type = shb.secretFileType;
};
config = {
myOption.source = "/a/path";
# Use the option.
environment.etc.myOption.text = config.myOption.source;
};
}
)
];
};
# This example shows how to apply patches to nixpkgs manually.
#
# Test with:
# nix build .#nixosConfigurations.manual.config.system.build.toplevel
# nix eval .#nixosConfigurations.manual.config.myOption
manual =
let
pkgs = import selfhostblocks.inputs.nixpkgs {
inherit system;
};
nixpkgs' = pkgs.applyPatches {
name = "nixpkgs-patched";
src = selfhostblocks.inputs.nixpkgs;
patches = selfhostblocks.lib.${system}.patches;
};
# We must import nixosSystem directly from the patched nixpkgs
# otherwise we do not get the patches.
nixosSystem' = import "${nixpkgs'}/nixos/lib/eval-config.nix";
in
nixosSystem' {
inherit system;
modules = [
selfhostblocks.nixosModules.default
filesystemModule
# This modules showcases the use of SHB's lib.
(
{
config,
lib,
shb,
...
}:
{
options.myOption = lib.mkOption {
# Using provided nixosSystem directly.
# SHB's lib is available under `shb` thanks to the overlay.
type = shb.secretFileType;
};
config = {
myOption.source = "/a/path";
# Use the option.
environment.etc.myOption.text = config.myOption.source;
};
}
)
];
};
# Test with:
# nix build .#nixosConfigurations.contractsDirect.config.system.build.toplevel
contractsDirect =
let
nixosSystem' = import "${selfhostblocks.inputs.nixpkgs}/nixos/lib/eval-config.nix";
in
nixosSystem' {
inherit system;
modules = [
filesystemModule
(import "${selfhostblocks}/lib/module.nix")
(
{
config,
lib,
shb,
...
}:
{
options.myOption = lib.mkOption {
# Using provided nixosSystem directly.
# SHB's lib is available under `shb` thanks to the overlay.
type = shb.secretFileType;
};
config = {
myOption.source = "/a/path";
# Use the option.
environment.etc.myOption.text = config.myOption.source;
};
}
)
];
};
};
};
}

View file

@ -112,7 +112,7 @@ ssh -F ssh_config example
:::: {.note} :::: {.note}
This section corresponds to the `basic` section of the [Nextcloud This section corresponds to the `basic` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloudserver-usage-basic). manual](services-nextcloud.html#services-nextcloud-server-usage-basic).
:::: ::::
Assuming you already deployed the `basic` demo, now you must add the following entry to the Assuming you already deployed the `basic` demo, now you must add the following entry to the
@ -143,7 +143,7 @@ This is the admin user of Nextcloud and that's the end of the `basic` demo.
:::: {.note} :::: {.note}
This section corresponds to the `ldap` section of the [Nextcloud This section corresponds to the `ldap` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloudserver-usage-ldap). manual](services-nextcloud.html#services-nextcloud-server-usage-ldap).
:::: ::::
Assuming you already deployed the `ldap` demo, now you must add the following entry to the Assuming you already deployed the `ldap` demo, now you must add the following entry to the
@ -172,8 +172,6 @@ Create the group `nextcloud_user` and a create a user and assign them to that gr
Finally, go to [http://n.example.com/login:8080](http://n.example.com/login:8080) and login with the user and Finally, go to [http://n.example.com/login:8080](http://n.example.com/login:8080) and login with the user and
password you just created above. password you just created above.
You might need to wait a minute or two until Nextcloud initialized correctly.
Until then, you'll get a 502 Bad Gateway error.
Nextcloud doesn't like being run without SSL protection, which this demo does not setup, so you Nextcloud doesn't like being run without SSL protection, which this demo does not setup, so you
might see errors loading scripts. See the `sso` demo for SSL. might see errors loading scripts. See the `sso` demo for SSL.
@ -184,12 +182,11 @@ This is the end of the `ldap` demo.
:::: {.note} :::: {.note}
This section corresponds to the `sso` section of the [Nextcloud This section corresponds to the `sso` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloudserver-usage-oidc). manual](services-nextcloud.html#services-nextcloud-server-usage-oidc).
:::: ::::
At this point, it is assumed you already deployed the `sso` demo. This time, we cannot simply edit local At this point, it is assumed you already deployed the `sso` demo. There is no host to add to
`/etc/hosts`, because Nextcloud SSO addon must be able to connect to Authelia by domain name `/etc/hosts` here. Instead, there is a `dnsmasq` server running in the VM and you must create a
(`auth.example.com`). Instead, there is a `dnsmasq` server running in the VM and you must create a
SOCKS proxy to connect to it like so: SOCKS proxy to connect to it like so:
```bash ```bash
@ -220,7 +217,7 @@ This is the end of the `sso` demo.
### Files {#demo-nextcloud-tips-files} ### Files {#demo-nextcloud-tips-files}
- [`flake.nix`](./flake.nix): nix entry point, defines the target hosts for - [`flake.nix`](./flake.nix): nix entry point, defines the target hosts for
[colmena](https://colmena.cli.rs) to deploy to as well as the selfhostblocks' config for setting [colmena](https://colmena.cli.rs) to deploy to as well as the selfhostblock's config for setting
up Nextcloud and the auxiliary services. up Nextcloud and the auxiliary services.
- [`configuration.nix`](./configuration.nix): defines all configuration required for colmena - [`configuration.nix`](./configuration.nix): defines all configuration required for colmena
to deploy to the VM. The file has comments if you're interested. to deploy to the VM. The file has comments if you're interested.

View file

@ -5,8 +5,8 @@ let
targetPort = 2222; targetPort = 2222;
in in
{ {
imports = [ imports =
# Include the results of the hardware scan. [ # Include the results of the hardware scan.
./hardware-configuration.nix ./hardware-configuration.nix
]; ];
@ -27,10 +27,7 @@ in
# Options above are needed to deploy in a VM. # Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ nix.settings.experimental-features = [ "nix-command" "flakes" ];
"nix-command"
"flakes"
];
# We need to create the user we will deploy with. # We need to create the user we will deploy with.
users.users.${targetUser} = { users.users.${targetUser} = {
@ -45,11 +42,9 @@ in
# The user we're deploying with must be able to run sudo without password. # The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [ security.sudo.extraRules = [
{ { users = [ targetUser ];
users = [ targetUser ];
commands = [ commands = [
{ { command = "ALL";
command = "ALL";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
]; ];
@ -65,9 +60,7 @@ in
services.openssh = { services.openssh = {
enable = true; enable = true;
ports = [ targetPort ]; ports = [ targetPort ];
settings = { permitRootLogin = "no";
PermitRootLogin = "no"; passwordAuthentication = false;
PasswordAuthentication = false;
};
}; };
} }

164
demo/nextcloud/flake.lock Normal file
View file

@ -0,0 +1,164 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-flake-tests": {
"locked": {
"lastModified": 1677844186,
"narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
"owner": "antifuchs",
"repo": "nix-flake-tests",
"rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
"type": "github"
},
"original": {
"owner": "antifuchs",
"repo": "nix-flake-tests",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714253743,
"narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1713638189,
"narHash": "sha256-q7APLfB6FmmSMI1Su5ihW9IwntBsk2hWNXh8XtSdSIk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "74574c38577914733b4f7a775dd77d24245081dd",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713596654,
"narHash": "sha256-LJbHQQ5aX1LVth2ST+Kkse/DRzgxlVhTL1rxthvyhZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd16bb6d3bcca96039b11aa52038fafeb6e4f4be",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nmdsrc": {
"flake": false,
"locked": {
"lastModified": 1705050560,
"narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"ref": "refs/heads/master",
"rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"revCount": 66,
"type": "git",
"url": "https://git.sr.ht/~rycee/nmd"
},
"original": {
"type": "git",
"url": "https://git.sr.ht/~rycee/nmd"
}
},
"root": {
"inputs": {
"selfhostblocks": "selfhostblocks"
}
},
"selfhostblocks": {
"inputs": {
"flake-utils": "flake-utils",
"nix-flake-tests": "nix-flake-tests",
"nixpkgs": "nixpkgs",
"nmdsrc": "nmdsrc",
"sops-nix": "sops-nix"
},
"locked": {
"dirtyRev": "710428c5a02a1b6c0789281c493e9555a16e4d22-dirty",
"dirtyShortRev": "710428c-dirty",
"lastModified": 1714374711,
"narHash": "sha256-bn8mImVyK0EQwWHeIK6bx6JvVVQVCnxaUt6aJqWigkY=",
"type": "git",
"url": "file:///home/timi/Projects/selfhostblocks"
},
"original": {
"owner": "ibizaman",
"repo": "selfhostblocks",
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1713892811,
"narHash": "sha256-uIGmA2xq41vVFETCF1WW4fFWFT2tqBln+aXnWrvjGRE=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "f1b0adc27265274e3b0c9b872a8f476a098679bd",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,36 +1,19 @@
{ {
description = "Nextcloud example for Self Host Blocks"; description = "Home Assistant example for Self Host Blocks";
inputs = { inputs = {
selfhostblocks.url = "github:ibizaman/selfhostblocks"; selfhostblocks.url = "github:ibizaman/selfhostblocks";
sops-nix.url = "github:Mic92/sops-nix";
}; };
outputs = outputs = inputs@{ self, selfhostblocks, ... }:
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let let
system = "x86_64-linux"; basic = { config, ... }: {
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
basic =
{ config, ... }:
{
imports = [ imports = [
./configuration.nix ./configuration.nix
selfhostblocks.nixosModules.authelia selfhostblocks.inputs.sops-nix.nixosModules.default
selfhostblocks.nixosModules.nextcloud-server selfhostblocks.nixosModules.x86_64-linux.default
selfhostblocks.nixosModules.nginx
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
]; ];
sops.defaultSopsFile = ./secrets.yaml;
shb.nextcloud = { shb.nextcloud = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
@ -42,53 +25,75 @@
# This option is only needed because we do not access Nextcloud at the default port in the VM. # This option is only needed because we do not access Nextcloud at the default port in the VM.
port = 8080; port = 8080;
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result; adminPassFile = config.sops.secrets."nextcloud/adminpass".path;
apps = { apps = {
previewgenerator.enable = true; previewgenerator.enable = true;
}; };
}; };
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
# Secret needed for services.nextcloud.config.adminpassFile.
sops.secrets."nextcloud/adminpass" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "nextcloud";
group = "nextcloud";
restartUnits = [ "phpfpm-nextcloud.service" ];
};
# Set to true for more debug info with `journalctl -f -u nginx`. # Set to true for more debug info with `journalctl -f -u nginx`.
shb.nginx.accessLog = true; shb.nginx.accessLog = true;
shb.nginx.debugLog = false; shb.nginx.debugLog = false;
}; };
ldap = ldap = { config, ... }: {
{ config, ... }: shb.ldap = {
{
shb.lldap = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "ldap"; subdomain = "ldap";
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; ldapUserPasswordFile = config.sops.secrets."lldap/user_password".path;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; jwtSecretFile = config.sops.secrets."lldap/jwt_secret".path;
};
sops.secrets."lldap/user_password" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
sops.secrets."lldap/jwt_secret" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
}; };
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.nextcloud.apps.ldap = { shb.nextcloud.apps.ldap = {
enable = true; enable = true;
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.lldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminName = "admin"; adminName = "admin";
adminPassword.result = config.shb.sops.secret."nextcloud/ldap_admin_password".result; adminPasswordFile = config.sops.secrets."nextcloud/ldap_admin_password".path;
userGroup = "nextcloud_user"; userGroup = "nextcloud_user";
}; };
shb.sops.secret."nextcloud/ldap_admin_password" = {
request = config.shb.nextcloud.apps.ldap.adminPassword.request; # Secret needed for LDAP app.
settings.key = "lldap/user_password"; sops.secrets."nextcloud/ldap_admin_password" = {
sopsFile = ./secrets.yaml;
key = "lldap/user_password";
mode = "0400";
owner = "nextcloud";
group = "nextcloud";
restartUnits = [ "nextcloud-setup.service" ];
}; };
}; };
sso = sso = { config, ... }: {
{ config, lib, ... }:
{
shb.certs = { shb.certs = {
cas.selfsigned.myca = { cas.selfsigned.myca = {
name = "My CA"; name = "My CA";
@ -97,15 +102,9 @@
n = { n = {
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.example.com"; domain = "*.example.com";
group = "nginx";
}; };
}; };
}; };
shb.nextcloud = {
port = lib.mkForce null;
ssl = config.shb.certs.certs.selfsigned.n;
};
shb.lldap.ssl = config.shb.certs.certs.selfsigned.n;
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
@ -113,7 +112,8 @@
domain-needed = true; domain-needed = true;
# no-resolv = true; # no-resolv = true;
bogus-priv = true; bogus-priv = true;
address = map (hostname: "/${hostname}/127.0.0.1") [ address =
map (hostname: "/${hostname}/127.0.0.1") [
"example.com" "example.com"
"n.example.com" "n.example.com"
"ldap.example.com" "ldap.example.com"
@ -127,32 +127,58 @@
domain = "example.com"; domain = "example.com";
subdomain = "auth"; subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
ldapPort = config.shb.lldap.ldapPort;
ldapHostname = "127.0.0.1"; ldapEndpoint = "ldap://127.0.0.1:${builtins.toString config.shb.ldap.ldapPort}";
dcdomain = config.shb.lldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
secrets = { secrets = {
jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result; jwtSecretFile = config.sops.secrets."authelia/jwt_secret".path;
ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result; ldapAdminPasswordFile = config.sops.secrets."authelia/ldap_admin_password".path;
sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result; sessionSecretFile = config.sops.secrets."authelia/session_secret".path;
storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result; storageEncryptionKeyFile = config.sops.secrets."authelia/storage_encryption_key".path;
identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result; identityProvidersOIDCHMACSecretFile = config.sops.secrets."authelia/hmac_secret".path;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result; identityProvidersOIDCIssuerPrivateKeyFile = config.sops.secrets."authelia/private_key".path;
}; };
}; };
shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request; sops.secrets."authelia/jwt_secret" = {
shb.sops.secret."authelia/ldap_admin_password" = { sopsFile = ./secrets.yaml;
request = config.shb.authelia.secrets.ldapAdminPassword.request; mode = "0400";
settings.key = "lldap/user_password"; owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
};
# Here we use the password defined in the lldap/user_password field in the secrets.yaml file
# and sops-nix will write it to "/run/secrets/authelia/ldap_admin_password".
sops.secrets."authelia/ldap_admin_password" = {
sopsFile = ./secrets.yaml;
key = "lldap/user_password";
mode = "0400";
owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
};
sops.secrets."authelia/session_secret" = {
sopsFile = ./secrets.yaml;
mode = "0400";
owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
};
sops.secrets."authelia/storage_encryption_key" = {
sopsFile = ./secrets.yaml;
mode = "0400";
owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
};
sops.secrets."authelia/hmac_secret" = {
sopsFile = ./secrets.yaml;
mode = "0400";
owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
};
sops.secrets."authelia/private_key" = {
sopsFile = ./secrets.yaml;
mode = "0400";
owner = config.shb.authelia.autheliaUser;
restartUnits = [ "authelia.service" ];
}; };
shb.sops.secret."authelia/session_secret".request =
config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request =
config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request =
config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request =
config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.nextcloud.apps.sso = { shb.nextcloud.apps.sso = {
enable = true; enable = true;
@ -160,13 +186,21 @@
clientID = "nextcloud"; clientID = "nextcloud";
fallbackDefaultAuth = true; fallbackDefaultAuth = true;
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result; secretFile = config.sops.secrets."nextcloud/sso/secret".path;
secretForAuthelia.result = config.shb.sops.secret."authelia/nextcloud_sso_secret".result; secretFileForAuthelia = config.sops.secrets."authelia/nextcloud_sso_secret".path;
}; };
shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request;
shb.sops.secret."authelia/nextcloud_sso_secret" = { sops.secrets."nextcloud/sso/secret" = {
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; sopsFile = ./secrets.yaml;
settings.key = "nextcloud/sso/secret"; mode = "0400";
owner = "nextcloud";
restartUnits = [ "nextcloud-setup.service" ];
};
sops.secrets."authelia/nextcloud_sso_secret" = {
sopsFile = ./secrets.yaml;
key = "nextcloud/sso/secret";
mode = "0400";
owner = config.shb.authelia.autheliaUser;
}; };
}; };
@ -177,14 +211,14 @@
in in
{ {
nixosConfigurations = { nixosConfigurations = {
basic = nixpkgs'.nixosSystem { basic = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
sopsConfig sopsConfig
basic basic
]; ];
}; };
ldap = nixpkgs'.nixosSystem { ldap = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
sopsConfig sopsConfig
@ -192,7 +226,7 @@
ldap ldap
]; ];
}; };
sso = nixpkgs'.nixosSystem { sso = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
sopsConfig sopsConfig
@ -205,15 +239,13 @@
colmena = { colmena = {
meta = { meta = {
nixpkgs = import nixpkgs' { nixpkgs = import selfhostblocks.inputs.nixpkgs {
system = "x86_64-linux"; system = "x86_64-linux";
}; };
specialArgs = inputs; specialArgs = inputs;
}; };
basic = basic = { config, ... }: {
{ config, ... }:
{
imports = [ imports = [
basic basic
]; ];
@ -225,9 +257,7 @@
}; };
}; };
ldap = ldap = { config, ... }: {
{ config, ... }:
{
imports = [ imports = [
basic basic
ldap ldap
@ -240,9 +270,7 @@
}; };
}; };
sso = sso = { config, ... }: {
{ config, ... }:
{
imports = [ imports = [
basic basic
ldap ldap

View file

@ -3,63 +3,50 @@
# Do not modify this file! It was generated by nixos-generate-config # Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes # and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead. # to /etc/nixos/configuration.nix instead.
{ { config, lib, pkgs, modulesPath, ... }:
config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = [ imports =
(modulesPath + "/profiles/qemu-guest.nix") [ (modulesPath + "/profiles/qemu-guest.nix")
]; ];
boot.initrd.availableKernelModules = [ boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
"ata_piix"
"uhci_hcd"
"virtio_pci"
"floppy"
"sr_mod"
"virtio_blk"
];
boot.initrd.kernelModules = [ ]; boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ]; boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
fileSystems."/" = { fileSystems."/" =
device = "/dev/vda"; { device = "/dev/vda";
fsType = "ext4"; fsType = "ext4";
}; };
fileSystems."/nix/.ro-store" = { fileSystems."/nix/.ro-store" =
device = "nix-store"; { device = "nix-store";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/nix/.rw-store" = { fileSystems."/nix/.rw-store" =
device = "tmpfs"; { device = "tmpfs";
fsType = "tmpfs"; fsType = "tmpfs";
}; };
fileSystems."/tmp/shared" = { fileSystems."/tmp/shared" =
device = "shared"; { device = "shared";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/tmp/xchg" = { fileSystems."/tmp/xchg" =
device = "xchg"; { device = "xchg";
fsType = "9p"; fsType = "9p";
}; };
fileSystems."/nix/store" = { fileSystems."/nix/store" =
device = "overlay"; { device = "overlay";
fsType = "overlay"; fsType = "overlay";
}; };
fileSystems."/boot" = { fileSystems."/boot" =
device = "/dev/vdb2"; { device = "/dev/vdb2";
fsType = "vfat"; fsType = "vfat";
}; };

View file

@ -8,6 +8,7 @@ lldap:
user_password: ENC[AES256_GCM,data:4ImmaC2T1hj6L8tzrxv4d7/I4F9xEA/uuc56QOqkY08=,iv:SljGhXi3SYoMNcR9onwqthOAyFX1D8KsegmWRypbblQ=,tag:Aw+juIV2AM0J+89itNDjVA==,type:str] user_password: ENC[AES256_GCM,data:4ImmaC2T1hj6L8tzrxv4d7/I4F9xEA/uuc56QOqkY08=,iv:SljGhXi3SYoMNcR9onwqthOAyFX1D8KsegmWRypbblQ=,tag:Aw+juIV2AM0J+89itNDjVA==,type:str]
jwt_secret: ENC[AES256_GCM,data:btABIOGRgioXmPe8QirhyozQzhVaAcF2sbB07hevz+Q=,iv:vBOq4Mab3RE69rOA8ZbMX72Gm3KEng6HaCveZrXsIrU=,tag:zkbJ+SeNnzQyAZxOjso8fg==,type:str] jwt_secret: ENC[AES256_GCM,data:btABIOGRgioXmPe8QirhyozQzhVaAcF2sbB07hevz+Q=,iv:vBOq4Mab3RE69rOA8ZbMX72Gm3KEng6HaCveZrXsIrU=,tag:zkbJ+SeNnzQyAZxOjso8fg==,type:str]
authelia: authelia:
ldap_admin_password: ENC[AES256_GCM,data:Ze1FJSl8ZJYCYrULlkwcbDFzxCS4MzujJbCGZasOiWU=,iv:X/su9ty9883+4qmrQhAIe6HDwjFoqHQ43aqd/4ZmtBw=,tag:QeLqUtYlZcHMR+bqRHCb2A==,type:str]
jwt_secret: ENC[AES256_GCM,data:xom/W92DGS2RafO+olwG8oKAbKPbkPKyZ2mYv0lWqtVAWUFwSoCGLgxe4uHAoGcLosJmDxU/srq+HNPzYORY8+mHn9wMoQgYg2oceLw2xamYdkIzvswof6LoYAV7MaZReYgYXcqMy2LZuU3PnnE4wag3liSuEx4qtJrLKB52ljE=,iv:t5PsBdZDze3/4S8utfnkmiToaorqq5BiJn99JuRirXY=,tag:ZJCszIOpaSwl9Sua8VWHoA==,type:str] jwt_secret: ENC[AES256_GCM,data:xom/W92DGS2RafO+olwG8oKAbKPbkPKyZ2mYv0lWqtVAWUFwSoCGLgxe4uHAoGcLosJmDxU/srq+HNPzYORY8+mHn9wMoQgYg2oceLw2xamYdkIzvswof6LoYAV7MaZReYgYXcqMy2LZuU3PnnE4wag3liSuEx4qtJrLKB52ljE=,iv:t5PsBdZDze3/4S8utfnkmiToaorqq5BiJn99JuRirXY=,tag:ZJCszIOpaSwl9Sua8VWHoA==,type:str]
storage_encryption_key: ENC[AES256_GCM,data:wUmF+0etuhEr3FNy7x0LBJunn1vmWO+IExm/wgkh0CEDWzxblpylC/PGAGgHdlJMQOhUY6tDPD67sJgO2g+yTBB3lfOo/kql0gnGVKQjRMMHqfEEmXK56yXP+J2JePJ6DlaqzdAXko4Tmh4GnRKsswMQZVA5PDOuHHNRcVTCb0E=,iv:wz1Mry7jMwGvD9mF1/PbQsHb/jmm8WOWchLL95YADeY=,tag:AZp43iti+nxW0TYK7MlYNg==,type:str] storage_encryption_key: ENC[AES256_GCM,data:wUmF+0etuhEr3FNy7x0LBJunn1vmWO+IExm/wgkh0CEDWzxblpylC/PGAGgHdlJMQOhUY6tDPD67sJgO2g+yTBB3lfOo/kql0gnGVKQjRMMHqfEEmXK56yXP+J2JePJ6DlaqzdAXko4Tmh4GnRKsswMQZVA5PDOuHHNRcVTCb0E=,iv:wz1Mry7jMwGvD9mF1/PbQsHb/jmm8WOWchLL95YADeY=,tag:AZp43iti+nxW0TYK7MlYNg==,type:str]
session_secret: ENC[AES256_GCM,data:TSe2YEyXl0Ls8wAynUYRJBQL8mbC1i/31ueuCj7d7ouO9gCX/Igz6OM9EgWigxucsMVQkiUtDCI9DD9B8jFaYGMIiB9FrKQnixigptrIUj210zJ3Aer38GyFxSI541PaBzmnauEo1MtBykjSg93xyI6ivB8FJmmauQOMYNiTYvk=,iv:OBtUCw7BevaF3VQKLJ2HiB828IzJqS27SZUOoAqoD+E=,tag:WfCGlHi6a15AYeSFXnnOVw==,type:str] session_secret: ENC[AES256_GCM,data:TSe2YEyXl0Ls8wAynUYRJBQL8mbC1i/31ueuCj7d7ouO9gCX/Igz6OM9EgWigxucsMVQkiUtDCI9DD9B8jFaYGMIiB9FrKQnixigptrIUj210zJ3Aer38GyFxSI541PaBzmnauEo1MtBykjSg93xyI6ivB8FJmmauQOMYNiTYvk=,iv:OBtUCw7BevaF3VQKLJ2HiB828IzJqS27SZUOoAqoD+E=,tag:WfCGlHi6a15AYeSFXnnOVw==,type:str]
@ -28,8 +29,8 @@ sops:
dTNrOUhzOSsvRnNSMC9VOTJaY1orWUEK8IcLk/4X7O+ZRosM7KNQNSEgyGkFklRw dTNrOUhzOSsvRnNSMC9VOTJaY1orWUEK8IcLk/4X7O+ZRosM7KNQNSEgyGkFklRw
YSutsre5OOEUx1X+hxzu2GF9I4DGcSAbQtzPYBq7qcwxUR+oIXiJyQ== YSutsre5OOEUx1X+hxzu2GF9I4DGcSAbQtzPYBq7qcwxUR+oIXiJyQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-03-17T00:29:32Z" lastmodified: "2024-01-22T06:30:53Z"
mac: ENC[AES256_GCM,data:eE3F1K/brgKMnixJQo/A/VYjafNLAGKuSq1n8857yjsiNnro/hwDy9jNKLH3a6/5DX/aOjMfZJzgH3ycb7f4771IohrWoDLjymaVdgJXsTITXZaLQyN+QHoOTRbXAJwG1f4Mr2kEAdwK7JLtu9TqX82o2DmBWNRxkkn1Kv5NjiA=,iv:OSAI0b4H40xbzKQbD6F2B5Xu/8enUIclfds8uYH/q3o=,tag:fYTnxx8IQYMyXAeVTUiQ+A==,type:str] mac: ENC[AES256_GCM,data:mdCpYLoaMcotuOU8qB7Gj+79ALG4d4HAR0Yw6Y3gf5SFUOc59B4WdK4A3+cgSm3dvRB8HCg9Vo9llEjiOBNVFpBgIjOvUeyAMYNi6ZndS/yr4x3NSL2rPz2s9c+0tm8Qg61T5RtYS/on+gWiUoA+lzXN2uFFWyo09fWF4N5EOQo=,iv:TgdI759YCkgmGAbUtgiV+NoT40Cg8+BcRGH0ZlQZ5SE=,tag:LGgFrlJPNpG+HzQLHDcDUQ==,type:str]
pgp: [] pgp: []
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.2 version: 3.8.1

View file

@ -1,74 +1,43 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Blocks {#blocks} # Blocks {#blocks}
Blocks help you self-host apps or services. They implement a specific function like backup or secure Blocks help you self-host apps or services. They implement a specific function like backup or secure
access through a subdomain. Each block is designed to be usable on its own and to fit nicely with access through a subdomain. Each block is designed to be usable on its own and to fit nicely with
others. others.
All blocks are implemented under the blocks folder [in the repository](@REPO@/modules/blocks). In practice, a block implements a [contract](contracts.html) that must be followed to implement a
specific self-hosting function. It also comes with a unit test and NixOS VM test suite to ensure any
implementation follows the contract.
All services in SHB document how to setup the various blocks provided here. As an example, let's take the HTTPS access block which allows for a service to be accessible through
For custom services or those not provided by SHB, a specific subdomain. In Nix terms, this block defines at minimum the inputs:
the [Expose a service Recipe](recipes-exposeService.html) explains how to use the blocks here.
## Authentication {#blocks-category-authentication} - subdomain,
- domain,
- and upstream address of the service.
```{=include=} chapters html:into-file=//blocks-authelia.html It defines no outputs but has one major side effect:
modules/blocks/authelia/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-lldap.html - the service should be accessible through HTTPS at `https://subdomain.domain`.
modules/blocks/lldap/docs/default.md
```
## Backup {#blocks-category-backup} Anything that provides the inputs and expected outputs and side effects defined by the block can be
used to fulfill its contract. In this example, we could use any of Nginx, Caddy, Haproxy or others.
```{=include=} chapters html:into-file=//blocks-borgbackup.html Self Host Blocks provides at least one implementation for each block and allows you to use your own
modules/blocks/borgbackup/docs/default.md implementation if you want to, as long as it passes the tests. You can then use blocks to improve
``` services you already have deployed.
```{=include=} chapters html:into-file=//blocks-restic.html ::: {.note}
modules/blocks/restic/docs/default.md Not all blocks are yet documented. You can find all available blocks [in the repository](@REPO@/modules/blocks).
``` :::
```{=include=} chapters html:into-file=//blocks-sanoid.html
modules/blocks/sanoid/docs/default.md
```
## Database {#blocks-category-database}
```{=include=} chapters html:into-file=//blocks-postgresql.html
modules/blocks/postgresql/docs/default.md
```
## Secrets {#blocks-category-secrets}
```{=include=} chapters html:into-file=//blocks-sops.html
modules/blocks/sops/docs/default.md
```
## Filesystem {#blocks-category-filesystem}
```{=include=} chapters html:into-file=//blocks-zfs.html
modules/blocks/zfs/docs/default.md
```
## Network {#blocks-category-network}
```{=include=} chapters html:into-file=//blocks-ssl.html ```{=include=} chapters html:into-file=//blocks-ssl.html
modules/blocks/ssl/docs/default.md modules/blocks/ssl/docs/default.md
``` ```
```{=include=} chapters html:into-file=//blocks-nginx.html ```{=include=} chapters html:into-file=//blocks-backup.html
modules/blocks/nginx/docs/default.md modules/blocks/backup/docs/default.md
``` ```
## Introspection {#blocks-category-introspection}
```{=include=} chapters html:into-file=//blocks-monitoring.html ```{=include=} chapters html:into-file=//blocks-monitoring.html
modules/blocks/monitoring/docs/default.md modules/blocks/monitoring/docs/default.md
``` ```
```{=include=} chapters html:into-file=//blocks-mitmdump.html
modules/blocks/mitmdump/docs/default.md
```

View file

@ -1,30 +1,15 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Contracts {#contracts} # Contracts {#contracts}
::: {.note}
An [RFC][] has been created which is the most up-to-date version of contracts.
The text here is still relevant although the implementation itself has changed a little bit.
[RFC]: https://github.com/NixOS/rfcs/pull/189
:::
A contract decouples modules that use a functionality from modules that provide it. A first A contract decouples modules that use a functionality from modules that provide it. A first
intuition for contracts is they are generally related to accessing a shared resource. intuition for contracts is they are generally related to accessing a shared resource.
A few examples of contracts are generating SSL certificates, creating a user or knowing which files A few examples of contracts are generating SSL certificates, creating a user or knowing which files
and folders to backup. and folders to backup. Indeed, when generating certificates, the service using those do not care how
Indeed, when generating certificates, the service using those do not care how they were created. they were created. They just need to know where the certificate files are located.
They just need to know where the certificate files are located.
A contract is made between a `requester` module and a `provider` module. In practice, a contract is a set of options that any user of a contract expects to exist. Also, the
For example, a `backup` contract can be made between the [Nextcloud service][] and the [Restic service][]. values of these options dictate the behavior of the implementation. This is enforced with NixOS VM
The former is the `requester` - the one wanted to be backed up - tests.
and the latter is the `provider` of the contract - the one backing up files.
The `backup contract` would then say which set of options the `requester` and `provider` modules
must use to talk to each other.
[Nextcloud service]: ./services-nextcloud.html
[Restic service]: ./blocks-restic.html
## Provided contracts {#contracts-provided} ## Provided contracts {#contracts-provided}
@ -34,537 +19,55 @@ as possible, reducing the quite thick layer that it is now.
Provided contracts are: Provided contracts are:
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates. - [SSL generator contract](contracts-ssl.html) to generate SSL certificates. Two implementations are provided: self-signed and Let's Encrypt.
Two providers are implemented: self-signed and Let's Encrypt.
- [Backup contract][] to backup directories.
Two providers are implemented: [BorgBackup][] and [Restic][].
- [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
Two providers are implemented: [BorgBackup][] and [Restic][].
- [Dataset Backup contract](contracts-datasetbackup.html) to backup ZFS datasets.
One provider is implemented: [Sanoid][]
- [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
One provider is implemented: [SOPS][].
- [Dashboard contract](contracts-dashboard.html) to show services in a nice user-facing dashboard.
One provider is implemented: [Homepage][].
[backup contract]: contracts-backup.html
[borgbackup]: blocks-borgbackup.html
[homepage]: services-homepage.html
[restic]: blocks-restic.html
[sanoid]: blocks-sanoid.html
[sops]: blocks-sops.html
```{=include=} chapters html:into-file=//contracts-ssl.html ```{=include=} chapters html:into-file=//contracts-ssl.html
modules/contracts/ssl/docs/default.md modules/contracts/ssl/docs/default.md
``` ```
```{=include=} chapters html:into-file=//contracts-backup.html ## Why do we need this new concept? {#contracts-why}
modules/contracts/backup/docs/default.md
```
```{=include=} chapters html:into-file=//contracts-databasebackup.html Currently in nixpkgs, every module needing access to a shared resource must implement the logic
modules/contracts/databasebackup/docs/default.md needed to setup that resource themselves. Similarly, if the module is mature enough to let the user
``` select a particular implementation, the code lives inside that module.
```{=include=} chapters html:into-file=//contracts-datasetbackup.html
modules/contracts/datasetbackup/docs/default.md
```
```{=include=} chapters html:into-file=//contracts-secret.html
modules/contracts/secret/docs/default.md
```
```{=include=} chapters html:into-file=//contracts-dashboard.html
modules/contracts/dashboard/docs/default.md
```
## Problem Statement {#contracts-why}
Currently in nixpkgs, every module accessing a shared resource
must either implement the logic needed to setup that resource themselves
or either instruct the user how to set it up themselves.
For example, this is what the Nextcloud module looks like.
It sets up the `nginx module` and a database,
letting you choose between multiple databases.
![](./assets/contracts_before.png "A module composed of a core logic and a lot of peripheral logic.") ![](./assets/contracts_before.png "A module composed of a core logic and a lot of peripheral logic.")
This has a few disadvantages: This has a few disadvantages:
_I'm using the Nextcloud module to make the following examples more concrete - This leads to a lot of **duplicated code**. If a module wants to support a new implementation of a
but this applies to all other modules._ contract, the maintainers of that module must write code to make that happen.
- This also leads to **tight coupling**. The code written by the maintainers cannot be reused in
- This leads to a lot of **duplicated code**. other modules, apart from copy pasting.
If the Nextcloud module wants to support a new type of database, - There is also a **lack of separation of concerns**. The maintainers of a service must be experts
the maintainer of the Nextcloud module must do the work.
And if another module wants to support it too,
the maintainers of that module cannot re-use easily the work
of the Nextcloud maintainer,
apart from copy-pasting and adapting the code.
- This also leads to **tight coupling**.
The code written to integrate Nextcloud with the Nginx reverse proxy
is hard to decouple and make generic.
Letting the user choose between Nginx and another reverse proxy
will require a lot of work.
- There is also a **lack of separation of concerns**.
The maintainers of a service must be experts
in all implementations they let the users choose from. in all implementations they let the users choose from.
- This is **not extendable**. - Finally, this is **not extensible**. If you, the user of the module, want to use another
If you, the user of the module, want to use another implementation that is not supported, you are out of luck. You can always dive into the module's
implementation that is not supported, you are out of luck. code and extend it, but that is not an optimal experience.
You can always dive into the module's code and extend it with a lot of `mkForce`,
but that is not an optimal experience.
- Finally, there is **no interoperability**.
It is not currently possible to integrate the Nextcloud module
with an existing database or reverse proxy or other type of shared resource
that already exists on a non-NixOS machine.
We do believe that the decoupling contracts provides helps alleviate all the issues outlined above We do believe that the decoupling contracts provides helps alleviate all the issues outlined above
which makes it an essential step towards better interoperability. which makes it an essential step towards more adoption of Nix, if only in the self hosting scene.
![](./assets/contracts_after.png "A module containing only logic using peripheral logic through contracts.") ![](./assets/contracts_after.png "A module containing only logic using peripheral logic through contracts.")
Indeed, contracts allow: Indeed, contracts allow:
- **Reuse of code**. - **Reuse of code**. Since the implementation of a contract lives outside of modules using it, using
Since the implementation of a contract lives outside of modules using it, that implementation elsewhere is trivial.
using the same implementation and code elsewhere without copy-pasting is trivial. - **Loose coupling**. Modules that use a contract do not care how they are implemented, as long as
- **Loose coupling**. the implementation follows the behavior outlined by the contract.
Modules that use a contract do not care how they are implemented - Full **separation of concerns** (see diagram below). Now, each party's concern is separated with a
as long as the implementation follows the behavior outlined by the contract. clear boundary. The maintainer of a module using a contract can be different from the maintainers
- Full **separation of concerns** (see diagram below). of the implementation, allowing them to be experts in their own respective fields. But more
Now, each party's concern is separated with a clear boundary. importantly, the contracts themselves can be created and maintained by the community.
The maintainer of a module using a contract can be different from the maintainers - Full **extensibility**. The final user themselves can choose an implementation, even new custom
of the implementation, allowing them to be experts in their own respective fields. implementations not available in nixpkgs, without changing existing code.
But more importantly, the contracts themselves can be created and maintained by the community. - Last but not least, **Testability**. Thanks to NixOS VM test, we can even go one step further by
- Full **extensibility**. ensuring each implementation of a contract, even custom ones, provides required options and
The final user themselves can choose an implementation, behaves as the contract requires.
even new custom implementations not available in nixpkgs, without changing existing code.
- **Incremental adoption**.
Contracts can help bridge a NixOS system with any non-NixOS one.
For that, one can hardcode a requester or provider module to match
how the non-NixOS system is configured.
The responsibility falls of course on the user to make sure both system agree on the configuration.
- Last but not least, **Testability**.
Thanks to NixOS VM test, we can even go one step further
by ensuring each implementation of a contract, even custom ones,
provides required options and behaves as the contract requires
thanks to generic NixOS tests.
For an example, see the [generic backup contract test][generic backup test]
and the [instantiated NixOS tests][instantiated backup test]
ensuring the providers do implement the contract correctly.
![](./assets/contracts_separationofconcerns.png "Separation of concerns thanks to contracts.") ![](./assets/contracts_separationofconcerns.png "Separation of concerns thanks to contracts.")
## Concept {#contracts-concept}
Conceptually, a contract is an attrset of options with a defined behavior.
Let's take a reduced `secret` contract as example.
This contract allows a `requester` module to ask for a secret
and a `provider` module to generate that secret outside of the nix store
and provide it back to the `requester`.
In this case, the options for the contract could look like so:
_The full secret contract can be found [here][secret contract]._
[secret contract]: ./contracts-secret.html
```nix
{ lib, ... }:
let
inherit (lib) mkOption;
inherit (lib.types) submodule str;
in
{
# Filled out by the requester module.
request = mkOption {
type = submodule {
options = {
owner = mkOption {
description = "Linux user owning the secret file.";
type = str;
};
};
};
};
# Filled out by the provider module.
result = mkOption {
type = submodule {
options = {
path = mkOption {
description = "Linux user owning the secret file.";
type = str;
};
};
};
};
# Options specific for each provider.
settings = mkOption {
type = submodule {
options = {
encryptedFile = mkOption {
description = "Encrypted file containing the secret.";
type = path;
};
};
};
};
}
```
Unfortunately, the contract needs to be more complicated to handle several constraints.
1. First, to fill out the contract,
the `requester` must set the defaults for the `request.*` options
and the `provider` for the `result.*` options.
Since one cannot do that after calling the `mkOption` function,
the `request` and `result` attributes must be functions
taking in the defaults as arguments.
2. Another constraint is a `provider` module of a contract
will need to work for several `requester` modules.
This means that the option to provide the contract will be an
`attrsOf` of something, not just plainly the contract.
Think of a provider for the secret contract,
if it didn't use `attrsOf`, one could only create an unique secret
for all the modules, which is not useful.
3. Also, one usually want the defaults
for the contract to be computed from some other option.
For a `provider` module, the options in the `result` could be computed
from the `name` provided in the `attrsOf`
or from a value given in the `request` or `setting` attrset.
For example, a `provider` module for the `secret` contract would want
something like the following in pseudo code:
```nix
services.provier = {
secret = mkOption {
type = attrsOf (submodule ({ name, ... }: {
result = {
path = mkOption {
type = str;
default = "/run/secrets/${name}";
};
};
}))
};
};
```
Another example is for a `provider` module for the `backup` contract
which would want the name of the restore script to depend on the path
to the repository it is backing up to.
This is necessary to differentiate which source to restore from
in case one wants to backup a same `requester` service
to multiple different repositories.
One could be local and another remote, for example.
```nix
services.provider = {
backup = mkOption {
type = attrsOf (submodule ({ name, config, ... }: {
settings = {
};
result = {
restoreScript = {
type = str;
default = "provider-restore-${name}-${config.settings.repository.path}";
};
};
}));
};
};
```
4. Finally, the last constraint, which is also the more demanding,
is we want to generate the documentation
for the options with `nixos-generate-config`.
For that, the complicated `default` we give to options
that depend on other options break the documentation generation.
So instead of using only `default`,
we must also define `defaultText` attributes.
This means the actual `mkRequest` and `mkResult` functions
must take twice as many arguments as there are option.
One for the `default` and the other for the `defaultText`.
This will not be shown in the following snippets as it is
already complicated enough.
These are all the justifications to why the final contract structure
is as presented in the next section.
It makes it harder to write, but much easier to use,
which is nice property.
## Schema {#contracts-schema}
A contract for a version of the [backup contract][] with less options
would look like so:
```nix
{ lib, ... }:
let
inherit (lib) mkOption;
inherit (lib.types) submodule str;
in
{
mkRequest =
{ owner ? "root",
}: mkOption {
default = {
inherit owner;
};
type = submodule {
options = {
owner = mkOption {
description = "Linux user owning the secret file.";
type = str;
default = owner;
};
};
};
};
mkResult =
{ path ? "/run/secrets/secret",
}: mkOption {
type = submodule {
options = {
path = mkOption {
description = "Linux user owning the secret file.";
type = str;
default = path;
};
};
};
};
}
```
Assuming the `services.requester` module needs to receive a password from the user
and wants to use the `secret contract` for that,
it would then setup the option like so:
```nix
{ pkgs, lib, ... }:
let
inherit (lib) mkOption;
inherit (lib.types) submodule;
contracts = pkgs.callPackage ./modules/contracts {};
mkRequester = requestCfg: {
request = contracts.secret.mkRequest requestCfg;
result = contracts.secret.mkResult {};
};
in
{
options.services.requester = {
password = mkOption {
description = "Password for the service.";
type = submodule {
options = mkRequester {
owner = "requester";
};
};
};
};
config = {
// Use config.services.requester.password.result.path
};
}
```
A provider that can create multiple secrets would have an `attrsOf` option
and use the contract in it like so:
```nix
let
inherit (lib) mkOption;
inherit (lib.types) attrsOf submodule;
contracts = pkgs.callPackage ./modules/contracts {};
mkProvider =
module:
{ resultCfg,
settings ? {},
}: {
request = contracts.secret.mkRequest {};
result = contracts.secret.mkResult resultCfg;
} // optionalAttrs (settings != {}) { inherit settings; };
in
{
options.services.provider = {
secrets = mkOption {
type = attrsOf (submodule ({ name, options, ... }: {
options = mkProvider {
resultCfg = {
path = "/run/secrets/${name}";
};
settings = mkOption {
description = "Settings specific to the Sops provider.";
type = attrsOf (submodule {
options = {
repository = mkOption {
};
};
});
default = {};
};
};
}));
};
};
}
```
The `mkRequester` and `mkProvider` are provided by Self Host Blocks
as they are generic, so the actual syntax is a little bit different.
They were copied here that way so the snippets were self-contained.
To see a full contract in action, the secret contract is a good example.
It is composed of:
- [the contract][secret contract ref],
- [the mkRequester and mkProvider][contract lib] functions,
- [a requester][],
- [a provider][].
[secret contract ref]: ./contracts-secret.html#contract-secret-options
[contract lib]: @REPO@/modules/contracts/default.nix
[a requester]: ./blocks-sops.html#blocks-sops-options-shb.sops.secret
[a provider]: ./services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.adminPass
## Contract Tests {#contracts-test}
To make sure all providers module of a contract have the same behavior,
generic NixOS VM tests exist per contract.
They are generic because they work on any module,
as long as the module implements the contract of course.
A simplified test for a secret contract would look like the following.
First, there is the generic test:
```nix
{ pkgs, lib, shb, ... }:
let
inherit (lib) getAttrFromPath setAttrByPath;
in
{ name,
configRoot,
settingsCfg,
modules ? [],
owner ? "root",
content ? "secretPasswordA",
}: shb.test.runNixOSTest {
inherit name;
nodes.machine = { config, ... }: {
imports = modules;
config = setAttrByPath configRoot {
secretA = {
request = {
inherit owner;
};
settings = settingsCfg content;
};
};
};
testScript = { nodes, ... }:
let
result = (getAttrFromPath configRoot nodes.machine)."A".result;
in
''
owner = machine.succeed("stat -c '%U' ${result.path}").strip()
if owner != "${owner}":
raise Exception(f"Owner should be '${owner}' but got '{owner}'")
content = machine.succeed("cat ${result.path}").strip()
if content != "${content}":
raise Exception(f"Content should be '${content}' but got '{content}'")
'';
}
```
This test is generic because it sets the `request` on an option
whose path is not yet known.
It achieves this by calling `setAttrByPath configRoot` where `configRoot`
is a path to a module, for example `[ "services" "provider" ]` for a module
whose root option is under `services.provider`.
This test validates multiple aspects of the contract:
- The provider must understand the options of the `request`.
Here `request.owner`.
- The provider correctly provides the expected result.
Here the location of the secret in the `result.path` option.
- The provider must behave as expected.
Here, the secret located at `result.path` must have the correct `owner`
and the correct `content`.
Instantiating the test for a given provider looks like so:
```nix
{
hardcoded_root = contracts.test.secret {
name = "hardcoded_root";
modules = [ ./modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ];
settingsCfg = secret: {
content = secret;
};
};
hardcoded_user = contracts.test.secret {
name = "hardcoded_user";
owner = "user";
modules = [ ./modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ];
settingsCfg = secret: {
content = secret;
};
};
}
```
Validating a new provider is then just a matter of extending the above snippet.
To see a full contract test in action, the test for backup contract is a good example.
It is composed of:
- the [generic test][generic backup test]
- and [instantiated tests][instantiated backup test] for some providers.
[generic backup test]: @REPO@/modules/contracts/backup/test.nix
[instantiated backup test]: @REPO@/test/contracts/backup.nix
## Videos {#contracts-videos}
Two videos exist of me presenting the topic,
the first at [NixCon North America in spring of 2024][NixConNA2024]
and the second at [NixCon in Berlin in fall of 2024][NixConBerlin2024].
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
## Are there contracts in nixpkgs already? {#contracts-nixpkgs} ## Are there contracts in nixpkgs already? {#contracts-nixpkgs}
Actually not quite, but close. There are some ubiquitous options in nixpkgs. Those I found are: Actually not quite, but close. There are some ubiquitous options in nixpkgs. Those I found are:

View file

@ -1,28 +1,8 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Contributing {#contributing} # Contributing {#contributing}
All issues and Pull Requests are welcome! All issues and Pull Requests are welcome!
- Use this project. Something does not make sense? Something's not working? For Pull Requests, if they are substantial changes, please open an issue to discuss the details
- Documentation. Something is not clear?
- New services. Have one of your preferred service not integrated yet?
- Better patterns. See something weird in the code?
For PRs, if they are substantial changes, please open an issue to
discuss the details first. More details in [the contributing section](https://shb.skarabox.com/contributing.html)
of the manual.
Issues that are being worked on are labeled with the [in progress][] label.
Before starting work on those, you might want to talk about it in the issue tracker
or in the [matrix][] channel.
The prioritized issues are those belonging to the [next milestone][milestone].
Those issues are not set in stone and I'd be very happy to solve
an issue an user has before scratching my own itch.
[in progress]: https://github.com/ibizaman/selfhostblocks/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22in%20progress%22
[matrix]: https://matrix.to/#/%23selfhostblocks%3Amatrix.org
[milestone]: https://github.com/ibizaman/selfhostblocks/milestones
first. first.
## Chat Support {#contributing-chat} ## Chat Support {#contributing-chat}
@ -41,6 +21,8 @@ gets updated. I intend to upstream to nixpkgs as much of those as makes sense.
Run all tests: Run all tests:
```bash ```bash
$ nix build .#checks.${system}.all
# or
$ nix flake check $ nix flake check
# or # or
$ nix run github:Mic92/nix-fast-build -- --skip-cached --flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)" $ nix run github:Mic92/nix-fast-build -- --skip-cached --flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)"
@ -53,67 +35,15 @@ $ nix build .#checks.${system}.modules
$ nix build .#checks.${system}.vm_postgresql_peerAuth $ nix build .#checks.${system}.vm_postgresql_peerAuth
``` ```
### Playwright Tests {#contributing-playwright-tests} Run one VM test interactively:
If the test includes playwright tests, you can see the playwright trace with:
```bash
$ nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
```
If the test fails, you won't have the output directory available.
Instead, run the test with `--keep-failed` and at the end you will see a line saying
```
Keeping build directory '/nix/var/nix/builds/nix-31776-4270051001/build'
```
Copy the path and run:
```bash
sudo cp -r /nix/var/nix/builds/nix-31776-4270051001/build/shared-xchg/trace trace && sudo chown -R $USER: trace
```
Now, open that file with playwright:
```bash
nix run .#playwright -- show-trace trace/0.zip
```
### Debug Tests {#contributing-debug-tests}
Run the test in driver interactive mode:
```bash ```bash
$ nix run .#checks.${system}.vm_postgresql_peerAuth.driverInteractive $ nix run .#checks.${system}.vm_postgresql_peerAuth.driverInteractive
``` ```
When you get to the shell, start the server and/or client with one of the following commands: When you get to the shell, run either `start_all()` or `test_script()`. The former just starts all
the VMs and service, then you can introspect. The latter also starts the VMs if they are not yet and
```bash then will run the test script.
server.start()
client.start()
start_all()
```
To run the test from the shell, use `test_script()`.
Note that if the test script ends in error,
the shell will exit and you will need to restart the VMs.
After the shell started, you will see lines like so:
```
SSH backdoor enabled, the machines can be accessed like this:
Note: this requires systemd-ssh-proxy(1) to be enabled (default on NixOS 25.05 and newer).
client: ssh -o User=root vsock/3
server: ssh -o User=root vsock/4
```
With the following command, you can directly access the server's nginx instance with your browser at `http://localhost:8000`:
```bash
ssh-keygen -R vsock/4; ssh -o User=root -L 8000:localhost:80 vsock/4
```
## Upload test results to CI {#contributing-upload} ## Upload test results to CI {#contributing-upload}
@ -126,18 +56,6 @@ After running the `nix-fast-build` command from the previous section, run:
$ find . -type l -name "result-vm_*" | xargs readlink | nix run nixpkgs#cachix -- push selfhostblocks $ find . -type l -name "result-vm_*" | xargs readlink | nix run nixpkgs#cachix -- push selfhostblocks
``` ```
## Upload package to CI {#contributing-upload-package}
In the rare case where a package must be built but cannot in CI,
for example because of not enough memory,
you can push the package directly to the cache with:
```bash
nix build .#checks.x86_64-linux.vm_karakeep_backup.nodes.server.services.karakeep.package
readlink result | nix run nixpkgs#cachix -- push selfhostblocks
```
## Deploy using colmena {#contributing-deploy-colmena} ## Deploy using colmena {#contributing-deploy-colmena}
```bash ```bash
@ -222,12 +140,6 @@ $ nix run nixpkgs#nvd -- diff \
$ nix run nixpkgs#openssl -- rand -hex 64 $ nix run nixpkgs#openssl -- rand -hex 64
``` ```
## Write code {#contributing-code}
```{=include=} chapters html:into-file=//service-implementation-guide.html
service-implementation-guide.md
```
## Links that helped {#contributing-links} ## Links that helped {#contributing-links}
While creating NixOS tests: While creating NixOS tests:

View file

@ -1,45 +1,30 @@
# Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673 # Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673
# Read these docs online at https://shb.skarabox.com. { pkgs
{ , buildPackages
pkgs, , lib
buildPackages, , nmdsrc
lib, , stdenv
nmdsrc, , documentation-highlighter
stdenv, , nixos-render-docs
documentation-highlighter,
nixos-render-docs,
release, , release
allModules, , allModules
version ? builtins.readFile ../VERSION,
substituteVersionIn,
modules,
}: }:
let let
shbPath = toString ./..; shbPath = toString ./..;
gitHubDeclaration = gitHubDeclaration = user: repo: subpath:
user: repo: subpath: let urlRef = "main";
let
urlRef = "main";
end = if subpath == "" then "" else "/" + subpath; end = if subpath == "" then "" else "/" + subpath;
in in {
{
url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}"; url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}";
name = "<${repo}${end}>"; name = "<${repo}${end}>";
}; };
ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url; ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url;
buildOptionsDocs = buildOptionsDocs = args@{ modules, includeModuleSystemOptions ? true, ... }:
{
modules,
filterOptionPath ? null,
}:
args:
let let
config = { config = {
_module.check = false; _module.check = false;
@ -60,57 +45,38 @@ let
}; };
}; };
options = lib.setAttrByPath filterOptionPath (lib.getAttrFromPath filterOptionPath eval.options); options = if includeModuleSystemOptions
in then eval.options
buildPackages.nixosOptionsDoc ( else builtins.removeAttrs eval.options [ "_module" ];
{ in buildPackages.nixosOptionsDoc ({
inherit options; inherit options;
transformOptions = transformOptions = opt:
opt: opt // {
opt
// {
# Clean up declaration sites to not refer to the Home Manager # Clean up declaration sites to not refer to the Home Manager
# source tree. # source tree.
declarations = map ( declarations = map (decl:
decl: gitHubDeclaration "ibizaman" "selfhostblocks"
gitHubDeclaration "ibizaman" "selfhostblocks" ( (lib.removePrefix "/" (lib.removePrefix shbPath (toString decl)))) opt.declarations;
lib.removePrefix "/" (lib.removePrefix shbPath (toString decl))
)
) opt.declarations;
}; };
} } // builtins.removeAttrs args [ "modules" "includeModuleSystemOptions" ]);
// builtins.removeAttrs args [ "includeModuleSystemOptions" ]
);
scrubbedModule = { scrubbedModule = {
_module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs); _module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs);
_module.check = false; _module.check = false;
}; };
allOptionsDocs = optionsDocs = buildOptionsDocs {
paths: modules = allModules ++ [ scrubbedModule ];
(buildOptionsDocs
{
modules = paths ++ allModules ++ [ scrubbedModule ];
filterOptionPath = [ "shb" ];
}
{
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
} includeModuleSystemOptions = false;
).optionsJSON; };
individualModuleOptionsDocs = individualModuleOptionsDocs = path: (buildOptionsDocs {
filterOptionPath: paths: modules = [ path scrubbedModule ];
(buildOptionsDocs
{
modules = paths ++ [ scrubbedModule ];
inherit filterOptionPath;
}
{
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
} includeModuleSystemOptions = false;
).optionsJSON; }).optionsJSON;
nmd = import nmdsrc { nmd = import nmdsrc {
inherit lib; inherit lib;
@ -118,15 +84,15 @@ let
# `nmd` uses to work around the broken stylesheets in # `nmd` uses to work around the broken stylesheets in
# `docbook-xsl-ns`, so we restore the patched version here. # `docbook-xsl-ns`, so we restore the patched version here.
pkgs = pkgs // { pkgs = pkgs // {
docbook-xsl-ns = pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; }; docbook-xsl-ns =
pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; };
}; };
}; };
outputPath = "share/doc/selfhostblocks"; outputPath = "share/doc/selfhostblocks";
manpage-urls = pkgs.writeText "manpage-urls.json" "{}"; manpage-urls = pkgs.writeText "manpage-urls.json" ''{}'';
in in stdenv.mkDerivation {
stdenv.mkDerivation {
name = "self-host-blocks-manual"; name = "self-host-blocks-manual";
nativeBuildInputs = [ nixos-render-docs ]; nativeBuildInputs = [ nixos-render-docs ];
@ -158,54 +124,41 @@ stdenv.mkDerivation {
${nmdsrc}/static/highlightjs/highlight.min.js \ ${nmdsrc}/static/highlightjs/highlight.min.js \
${nmdsrc}/static/highlightjs/highlight.load.js ${nmdsrc}/static/highlightjs/highlight.load.js
''
+ lib.concatStringsSep "\n" (
map (m: ''
substituteInPlace ${m} --replace '@VERSION@' ${version}
'') substituteVersionIn
)
+ ''
substituteInPlace ./options.md \ substituteInPlace ./options.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${ ${optionsDocs.optionsJSON}/share/doc/nixos/options.json
allOptionsDocs [
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix") substituteInPlace ./modules/blocks/ssl/docs/default.md \
] --replace \
}/share/doc/nixos/options.json
''
+ lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: cfg':
let
cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; };
module = if builtins.isList cfg.module then cfg.module else [ cfg.module ];
optionRoot =
cfg.optionRoot or [
"shb"
(lib.last (lib.splitString "/" name))
];
in
''
substituteInPlace ./modules/${name}/docs/default.md \
--replace-fail \
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json ${individualModuleOptionsDocs ../modules/blocks/ssl.nix}/share/doc/nixos/options.json
''
) modules substituteInPlace ./modules/blocks/backup/docs/default.md \
) --replace \
+ '' '@OPTIONS_JSON@' \
${individualModuleOptionsDocs ../modules/blocks/backup.nix}/share/doc/nixos/options.json
substituteInPlace ./modules/services/nextcloud-server/docs/default.md \
--replace \
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs ../modules/services/nextcloud-server.nix}/share/doc/nixos/options.json
substituteInPlace ./modules/contracts/ssl/docs/default.md \
--replace \
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs ../modules/contracts/ssl/dummyModule.nix}/share/doc/nixos/options.json
find . -name "*.md" -print0 | \ find . -name "*.md" -print0 | \
while IFS= read -r -d ''' f; do while IFS= read -r -d ''' f; do
substituteInPlace "''${f}" \ substituteInPlace "''${f}" \
--replace-quiet \ --replace \
'@REPO@' \ '@REPO@' \
"${ghRoot}" 2>/dev/null "${ghRoot}" 2>/dev/null
done done
nixos-render-docs manual html \ nixos-render-docs manual html \
--manpage-urls ${manpage-urls} \ --manpage-urls ${manpage-urls} \
--redirects ./redirects.json \
--media-dir media \ --media-dir media \
--revision ${lib.trivial.revisionWithDefault release} \ --revision ${lib.trivial.revisionWithDefault release} \
--stylesheet static/style.css \ --stylesheet static/style.css \

View file

@ -1,4 +1,3 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Demos {#demos} # Demos {#demos}
These demos are showcasing what Self Host Blocks can do. They deploy a block or a service on a VM on These demos are showcasing what Self Host Blocks can do. They deploy a block or a service on a VM on
@ -8,6 +7,6 @@ your local machine with minimal manual steps.
demo/homeassistant/README.md demo/homeassistant/README.md
``` ```
```{=include=} chapters html:into-file=//demo-nextcloud.html ```{=include=} chapters html:into-file=//demo-nextcloud-server.html
demo/nextcloud/README.md demo/nextcloud/README.md
``` ```

View file

@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""
Generate redirects.json by scanning actual HTML files produced by nixos-render-docs.
This script implements a runtime patching mechanism to automatically generate a
complete redirects.json file by scanning generated HTML files for real anchor
locations, eliminating manual maintenance and ensuring accuracy.
ARCHITECTURE OVERVIEW:
The script works by monkey-patching nixos-render-docs at runtime to:
1. Disable redirect validation during HTML generation
2. Generate HTML documentation normally
3. Scan all generated HTML files to extract anchor IDs and their file locations
4. Apply filtering logic to exclude system-generated anchors
5. Generate and write redirects.json with accurate mappings
KEY COMPONENTS:
- Runtime patching: Modifies nixos-render-docs behavior without source changes
- HTML scanning: Extracts anchor IDs using regex pattern matching
- Filtering: Excludes NixOS options (opt-*) and extra options (selfhostblock*)
- Output generation: Creates both debug information and production redirects.json
IMPORTANT NOTES:
- Uses atexit handler to ensure output is generated even if process is interrupted
- Patches are applied on module import, making this a side-effect import
- Error handling preserves original validation behavior in case of failure
"""
import sys
import json
import atexit
import os
import re
# Global storage for anchor-to-file mappings discovered during HTML scanning
# Structure: {anchor_id: html_filename}
file_target_mapping = {}
def scan_html_files(output_dir, html_files):
"""
Scan HTML files to extract anchor IDs and build anchor-to-file mappings.
Discovers all HTML files in output_dir and extracts id attributes to populate
the global file_target_mapping. Filters out NixOS system options during scanning.
Args:
output_dir: Directory containing generated HTML files
html_files: Unused parameter (always discovers files from filesystem)
"""
# Always discover HTML files from the output directory
if not os.path.exists(output_dir):
print(f"DEBUG: Output directory {output_dir} does not exist", file=sys.stderr)
return
html_files = [f for f in os.listdir(output_dir) if f.endswith('.html')]
print(f"DEBUG: Discovered {len(html_files)} HTML files in {output_dir}", file=sys.stderr)
# Process each HTML file to extract anchor IDs
for html_file in html_files:
html_path = os.path.join(output_dir, html_file)
try:
with open(html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# Extract all id attributes using regex pattern matching
# Matches: id="anchor-name" and captures anchor-name
anchor_matches = re.findall(r'id="([^"]+)"', html_content)
# Filter and record anchor mappings
non_opt_count = 0
for anchor_id in anchor_matches:
# Skip NixOS system option anchors (opt-* prefix)
if not anchor_id.startswith('opt-'):
file_target_mapping[anchor_id] = html_file
non_opt_count += 1
if non_opt_count > 0:
print(f"Found {non_opt_count} anchors in {html_file}", file=sys.stderr)
except Exception as e:
# Log errors but continue processing other files
print(f"Failed to scan {html_path}: {e}", file=sys.stderr)
def output_collected_refs():
"""
Generate and write the final redirects.json file from collected anchor mappings.
This function is registered as an atexit handler to ensure output is generated
even if the process is interrupted. It processes the global file_target_mapping
to create the final redirects file with appropriate filtering.
Output files:
- out/redirects.json: Production redirects mapping
"""
import os
# Generate redirects from discovered HTML anchor mappings
if file_target_mapping:
print(f"Creating redirects from {len(file_target_mapping)} HTML mappings", file=sys.stderr)
redirects = {}
filtered_count = 0
# Apply filtering logic to exclude system-generated anchors
for anchor_id, html_file in file_target_mapping.items():
# Filter out:
# - opt-*: NixOS system options
# - selfhostblock*: Extra options from this project
if not anchor_id.startswith('opt-') and not anchor_id.startswith('selfhostblock'):
redirects[anchor_id] = [f"{html_file}#{anchor_id}"]
else:
filtered_count += 1
print(f"Generated {len(redirects)} redirects (filtered out {filtered_count} system options)", file=sys.stderr)
else:
# Fallback case - should not occur during normal operation
print("Warning: No HTML mappings available", file=sys.stderr)
redirects = {}
# Ensure output directory exists
os.makedirs('out', exist_ok=True)
# Write production redirects file
try:
redirects_file = 'out/redirects.json'
with open(redirects_file, 'w') as f:
json.dump(redirects, f, indent=2, sort_keys=True)
print(f"Generated redirects.json with {len(redirects)} redirects", file=sys.stderr)
except Exception as e:
print(f"Failed to write redirects.json: {e}", file=sys.stderr)
# Register output generation to run on process exit
atexit.register(output_collected_refs)
def apply_patches():
"""
Apply runtime monkey patches to nixos-render-docs modules.
This function modifies the behavior of nixos-render-docs by:
1. Hooking into the HTML generation CLI command
2. Temporarily disabling redirect validation during HTML generation
3. Scanning generated HTML files to extract anchor mappings
4. Restoring original validation behavior
The patching approach allows us to extract anchor information without
modifying the nixos-render-docs source code directly.
Raises:
ImportError: If nixos-render-docs modules cannot be imported
"""
try:
# Import required nixos-render-docs modules
import nixos_render_docs.html as html_module
import nixos_render_docs.redirects as redirects_module
import nixos_render_docs.manual as manual_module
# Store reference to original HTML CLI function
original_run_cli_html = manual_module._run_cli_html
def patched_run_cli_html(args):
"""
Patched version of _run_cli_html that disables validation and scans output.
This wrapper function:
1. Temporarily disables redirect validation to prevent errors
2. Runs normal HTML generation
3. Scans generated HTML files for anchor mappings
4. Restores original validation behavior
"""
print("Generating HTML documentation...", file=sys.stderr)
# Temporarily disable redirect validation
original_validate = redirects_module.Redirects.validate
redirects_module.Redirects.validate = lambda self, targets: None
try:
# Run original HTML generation
result = original_run_cli_html(args)
# Determine output directory from CLI arguments
if hasattr(args, 'outfile') and args.outfile:
output_dir = os.path.dirname(args.outfile)
else:
output_dir = '.'
# Scan generated HTML files for anchor mappings
scan_html_files(output_dir, None)
print(f"Scanned {len(file_target_mapping)} anchor mappings", file=sys.stderr)
finally:
# Always restore original validation function
redirects_module.Redirects.validate = original_validate
return result
# Replace the original function with our patched version
manual_module._run_cli_html = patched_run_cli_html
print("Applied patches to nixos-render-docs", file=sys.stderr)
except ImportError as e:
print(f"Failed to apply patches: {e}", file=sys.stderr)
# Apply patches immediately when this module is imported
# This ensures the patches are active before nixos-render-docs CLI runs
apply_patches()

View file

@ -1,7 +1,6 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Self Host Blocks Manual {#self-host-blocks-manual} # Self Host Blocks Manual {#self-host-blocks-manual}
## Version @VERSION@ ## Version 0.0.1
```{=include=} preface ```{=include=} preface
@ -24,14 +23,6 @@ contracts.md
blocks.md blocks.md
``` ```
```{=include=} chapters html:into-file=//recipes.html
recipes.md
```
```{=include=} chapters html:into-file=//misc.html
misc.md
```
```{=include=} chapters html:into-file=//demos.html ```{=include=} chapters html:into-file=//demos.html
demos.md demos.md
``` ```

View file

@ -1,39 +0,0 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Miscellaneous {#misc}
Here goes meta-discussions around the repository and other topics not related directly to the usage of SHB.
## Lock file update {#misc-lock-file-update}
*SHB has an unusual strategy to keep up with its flake inputs. This section explains why.*
SHB only depends on `nixpkgs` as far as flake inputs goes.
There are others but they only matter for tasks around taking care of the repository itself.
So only the `nixpkgs` input is important for downstream consumers of SHB.
To keep up to date with nixpkgs unstable,
initially a job was updating the nixpkgs input by simply updating the lock file and creating a PR.
Now, this job failed most of the time because the tip of unstable often breaks modules and packages, resulting in failing SHB tests.
To fix this, we usually needed to choose a commit around 2 weeks prior to the latest unstable commit.
This usually did not lead to successful tests but the reason was then not
because of failing packages but because nixpkgs modules evolved and SHB needs to adapt to that.
The net effect was SHB was lagging too far behind unstable and updating it was becoming annoying.
The new strategy now is as follows. On every job run:
- If there is no PR, update nixpkgs input to latest unstable commit and create a PR which auto-merges when tests are successful.
- If there is a PR:
- If the checks succeeded, do nothing because the PR will auto-merge.
- If the checks are pending, do nothing yet.
- If the checks failed, bisect and update nixpkgs input to between the commit on main and the commit in the PR.
The effect here is as long as the tests are failing, we'll try a commit further in the past from nixpkgs unstable
and closer to the tip of main branch.
This could be made smarter by distinguishing between types of failures.
We will see if this is needed.
One can also run the bisect manually with `nix run .#update-flake-lock-pr`.
This will create a PR on a different branch than the one used for the automated workflow.
It accepts also one argument `future` which instead of trying a commit in between the tip of SHB main and the PR's failing one,
it tries a commit between the PR's failing one and the nixpkgs unstable latest commit.

View file

@ -1,8 +1,7 @@
<!-- Read these docs at https://shb.skarabox.com --> # All Options {#ch-options}
# All Options {#all-options}
```{=include=} options ```{=include=} options
id-prefix: opt- id-prefix: opt-
list-id: selfhostblocks-options list-id: selfhostblock-options
source: @OPTIONS_JSON@ source: @OPTIONS_JSON@
``` ```

View file

@ -1,405 +1,28 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Preface {#preface} # Preface {#preface}
::: {.note} ::: {.note}
Self Host Blocks is hosted on [GitHub](https://github.com/ibizaman/selfhostblocks). Self Host Blocks is hosted on [GitHub](https://github.com/ibizaman/selfhostblocks). If you encounter
If you encounter problems or bugs then please report them on the [issue problems or bugs then please report them on the [issue
tracker](https://github.com/ibizaman/selfhostblocks/issues). tracker](https://github.com/ibizaman/selfhostblocks/issues).
Feel free to join the dedicated Matrix room Feel free to join the dedicated Matrix room
[matrix.org#selfhostblocks](https://matrix.to/#/#selfhostblocks:matrix.org). [matrix.org#selfhostblocks](https://matrix.to/#/#selfhostblocks:matrix.org).
::: :::
SelfHostBlocks is: Self Host Blocks intends to help you self host any service you would like with best practices out of
the box.
- Your escape from the cloud, for privacy and data sovereignty enthusiast. [Why?](#preface-why-self-hosting)
- A groupware to self-host [all your data](#preface-services): documents, pictures, calendars, contacts, etc. To achieve this, Self Host Blocks provides building blocks which each provide part of what a self
- An opinionated NixOS server management OS for a [safe self-hosting experience](#preface-features). hosted app should do (SSO, HTTPS, etc.). It also provides some services that are already integrated
- A NixOS distribution making sure all services build and work correctly thanks to NixOS VM tests. with all those building blocks.
- A collection of NixOS modules standardizing options so configuring services [look the same](#preface-unified-interfaces).
- A testing ground for [contracts](#preface-contracts) which intents to make nixpkgs modules more modular. - You are new to self hosting and want pre-configured services to deploy easily. Look at the
- [Upstreaming][] as much as possible. [services section](services.html).
- You are a seasoned self-hoster but want to enhance some services you deploy already. Go to the
[upstreaming]: https://github.com/pulls?page=1&q=created%3A%3E2023-06-01+is%3Apr+author%3Aibizaman+archived%3Afalse+-repo%3Aibizaman%2Fselfhostblocks+-repo%3Aibizaman%2Fskarabox [blocks section](blocks.html).
- You are a user of Self Host Blocks but would like to use your own implementation for a block. Head
## Why Self-Hosting {#preface-why-self-hosting} over to the [matrix channel](https://matrix.to/#/#selfhostblocks:matrix.org) (the manual for this
is WIP).
It is obvious by now that
a deep dependency on proprietary service providers - "the cloud" - Self Host Blocks uses the full power of NixOS modules to achieve these goals. Blocks and service are
is a significant liability. both NixOS modules.
One aspect often talked about is privacy
which is inherently not guaranteed when using a proprietary service
and is a valid concern.
A more punishing issue is having your account closed or locked
without prior warning
When that happens,
you get an instantaneous sinking feeling in your stomach
at the realization you lost access to your data,
possibly without recourse.
Hosting services yourself is the obvious alternative
to alleviate those concerns
but it tends to require a lot of technical skills and time.
SelfHostBlocks (together with its sibling project [Skarabox][])
aims to lower the bar to self-hosting,
and provides an opinionated server management system based on NixOS modules
embedding best practices.
Contrary to other server management projects,
its main focus is ease of long term maintenance
before ease of installation.
To achieve this, it provides building blocks to setup services.
Some are already provided out of the box,
and customizing or adding additional ones is done easily.
The building blocks fit nicely together thanks to [contracts](#contracts)
which SelfHostBlocks sets out to introduce into nixpkgs.
This will increase modularity, code reuse
and empower end users to assemble components
that fit together to build their server.
## Usage {#preface-usage}
> **Caution:** You should know that although I am using everything in this repo for my personal
> production server, this is really just a one person effort for now and there are most certainly
> bugs that I didn't discover yet.
To get started using SelfHostBlocks, the following snippet is enough:
```nix
{
inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks";
outputs = { selfhostblocks, ... }: let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
in
nixosConfigurations = {
myserver = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
./configuration.nix
];
};
};
}
```
SelfHostBlocks provides its own patched nixpkgs, so you are required to use it
otherwise evaluation can quickly break.
[The usage section](https://shb.skarabox.com/usage.html) of the manual has
more details and goes over how to deploy with [Colmena][], [nixos-rebuild][] and [deploy-rs][]
and also how to handle secrets management with [SOPS][].
[Colmena]: https://shb.skarabox.com/usage.html#usage-example-colmena
[nixos-rebuild]: https://shb.skarabox.com/usage.html#usage-example-nixosrebuild
[deploy-rs]: https://shb.skarabox.com/usage.html#usage-example-deployrs
[SOPS]: https://shb.skarabox.com/usage.html#usage-secrets
Then, to actually configure services, you can choose which one interests you in
the [services section](https://shb.skarabox.com/services.html) of the manual.
The [recipes section](https://shb.skarabox.com/recipes.html) of the manual shows some other common use cases.
Head over to the [matrix channel](https://matrix.to/#/#selfhostblocks:matrix.org)
for any remaining question, or just to say hi :)
### Installation From Scratch {#preface-usage-installation-from-scratch}
I do recommend for this my sibling project [Skarabox][]
which bootstraps a new server and sets up a few tools:
- Create a bootable ISO, installable on an USB key.
- Handles one or two (in raid 1) SSDs for root partition.
- Handles two (in raid 1) or more hard drives for data partition.
- [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) to install NixOS headlessly.
- [disko](https://github.com/nix-community/disko) to format the drives using native ZFS encryption with remote unlocking through ssh.
- [sops-nix](https://github.com/Mic92/sops-nix) to handle secrets.
- [deploy-rs](https://github.com/serokell/deploy-rs) to deploy updates.
[Skarabox]: https://github.com/ibizaman/skarabox
## Features {#preface-features}
SelfHostBlocks provides building blocks that take care of common self-hosting needs:
- Backup for all services.
- Automatic creation of ZFS datasets per service.
- LDAP and SSO integration for most services.
- Monitoring with Grafana and Prometheus stack with provided dashboards.
- Automatic reverse proxy and certificate management for HTTPS.
- VPN and proxy tunneling services.
Great care is taken to make the proposed stack robust.
This translates into a test suite comprised of automated NixOS VM tests
which includes playwright tests to verify some important workflow
like logging in.
This test suite also serves as a guaranty that all services provided by SelfHostBlocks
all evaluate, build and work correctly together. It works similarly as a distribution but here it's all [automated](#preface-updates).
Also, the stack fits together nicely thanks to [contracts](#preface-contracts).
### Services {#preface-services}
[Provided services](https://shb.skarabox.com/services.html) are:
- Nextcloud
- Audiobookshelf
- Deluge + *arr stack
- Forgejo
- Grocy
- Hledger
- Home-Assistant
- Jellyfin
- Karakeep
- Open WebUI
- Pinchflat
- Vaultwarden
Like explained above, those services all benefit from
out of the box backup,
LDAP and SSO integration,
monitoring with Grafana,
reverse proxy and certificate management
and VPN integration for the *arr suite.
Some services do not have an entry yet in the manual.
To know options for those, the only way for now
is to go to the [All Options][] section of the manual.
[All Options]: https://shb.skarabox.com/options.html
### Blocks {#preface-blocks}
The services above rely on the following [common blocks][]
which altogether provides a solid foundation for self-hosting services:
- Authelia
- BorgBackup
- Davfs
- LDAP
- Monitoring (Grafana - Prometheus - Loki stack)
- Nginx
- PostgreSQL
- Restic
- Sops
- SSL
- Tinyproxy
- VPN
- ZFS
Those blocks can be used with services
not provided by SelfHostBlocks as shown [in the manual][common blocks].
[common blocks]: https://shb.skarabox.com/blocks.html
The manual also provides documentation for each individual blocks.
### Unified Interfaces {#preface-unified-interfaces}
Thanks to the blocks,
SelfHostBlocks provides an unified configuration interface
for the services it provides.
Compare the configuration for Nextcloud and Forgejo.
The following snippets focus on similitudes and assume the relevant blocks - like secrets - are configured off-screen.
It also does not show specific options for each service.
These are still complete snippets that configure HTTPS,
subdomain serving the service, LDAP and SSO integration.
```nix
shb.nextcloud = {
enable = true;
subdomain = "nextcloud";
domain = "example.com";
ssl = config.shb.certs.certs.letsencrypt.${domain};
apps.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
};
apps.sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."nextcloud/sso/secretForAuthelia".result;
};
};
```
```nix
shb.forgejo = {
enable = true;
subdomain = "forgejo";
domain = "example.com";
ssl = config.shb.certs.certs.letsencrypt.${domain};
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
};
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secret.result = config.shb.sops.secret."forgejo/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."forgejo/sso/secretForAuthelia".result;
};
};
```
As you can see, they are pretty similar!
This makes setting up a new service pretty easy and intuitive.
SelfHostBlocks provides an ever growing list of [services](#preface-services)
that are configured in the same way.
### Contracts {#preface-contracts}
To make building blocks that fit nicely together,
SelfHostBlocks pioneers [contracts][] which allows you, the final user,
to be more in control of which piece goes where.
This lets you choose, for example,
any reverse proxy you want or any database you want,
without requiring work from maintainers of the services you want to self host.
An [RFC][] exists to upstream this concept into `nixpkgs`.
The [manual][contracts] also provides an explanation of the why and how of contracts.
Also, two videos exist of me presenting the topic,
the first at [NixCon North America in spring of 2024][NixConNA2024]
and the second at [NixCon in Berlin in fall of 2024][NixConBerlin2024].
[contracts]: https://shb.skarabox.com/contracts.html
[RFC]: https://github.com/NixOS/rfcs/pull/189
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
### Interfacing With Other OSes {#preface-interface}
Thanks to [contracts](#contracts), one can interface NixOS
with systems on other OSes.
The [RFC][] explains how that works.
### Sitting on the Shoulders of a Giant {#preface-giants}
By using SelfHostBlocks, you get all the benefits of NixOS
which are, for self hosted applications specifically:
- declarative configuration;
- atomic configuration rollbacks;
- real programming language to define configurations;
- create your own higher level abstractions on top of SelfHostBlocks;
- integration with the rest of nixpkgs;
- much fewer "works on my machine" type of issues.
### Automatic Updates {#preface-updates}
SelfHostBlocks follows nixpkgs unstable branch closely.
There is a GitHub action running every couple of days that updates
the `nixpkgs` input in the root `flakes.nix`,
runs the tests and merges the PR automatically
if the tests pass.
A release is then made every few commits,
whenever deemed sensible.
On your side, to update I recommend pinning to a release
with the following command,
replacing the RELEASE with the one you want:
```bash
RELEASE=0.2.4
nix flake update \
--override-input selfhostblocks github:ibizaman/selfhostblocks/$RELEASE \
selfhostblocks
```
### Demos {#preface-demos}
Demos that start and deploy a service
on a Virtual Machine on your computer are located
under the [demo](./demo/) folder.
These show the onboarding experience you would get
if you deployed one of the services on your own server.
## Roadmap {#preface-roadmap}
Currently, the Nextcloud and Vaultwarden services
and the SSL and backup blocks
are the most advanced and most documented.
Documenting all services and blocks will be done
as I make all blocks and services use the contracts.
Upstreaming changes is also on the roadmap.
Check the [issues][] and the [milestones]() to see planned work.
Feel free to add more or to contribute!
[issues]: (https://github.com/ibizaman/selfhostblocks/issues)
[milestones]: https://github.com/ibizaman/selfhostblocks/milestones
All blocks and services have NixOS tests.
Also, I am personally using all the blocks and services in this project, so they do work to some extent.
## Community {#preface-community}
This project has been the main focus
of my (non work) life for the past 3 year now
and I intend to continue working on this for a long time.
All issues and PRs are welcome:
- Use this project. Something does not make sense? Something's not working?
- Documentation. Something is not clear?
- New services. Have one of your preferred service not integrated yet?
- Better patterns. See something weird in the code?
For PRs, if they are substantial changes, please open an issue to
discuss the details first. More details in [the contributing section](https://shb.skarabox.com/contributing.html)
of the manual.
Issues that are being worked on are labeled with the [in progress][] label.
Before starting work on those, you might want to talk about it in the issue tracker
or in the [matrix][] channel.
The prioritized issues are those belonging to the [next milestone][milestone].
Those issues are not set in stone and I'd be very happy to solve
an issue an user has before scratching my own itch.
[in progress]: https://github.com/ibizaman/selfhostblocks/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22in%20progress%22
[matrix]: https://matrix.to/#/%23selfhostblocks%3Amatrix.org
[milestone]: https://github.com/ibizaman/selfhostblocks/milestones
One aspect that's close to my heart is I intent to make SelfHostBlocks the lightest layer on top of nixpkgs as
possible. I want to upstream as much as possible. I will still take some time to experiment here but
when I'm satisfied with how things look, I'll upstream changes.
## Funding {#preface-funding}
I was lucky to [obtain a grant][nlnet] from NlNet which is an European fund,
under [NGI Zero Core][NGI0],
to work on this project.
This also funds the contracts RFC.
Go apply for a grant too!
[nlnet]: https://nlnet.nl/project/SelfHostBlocks
[NGI0]: https://nlnet.nl/core/
## License {#preface-license}
I'm following the [Nextcloud](https://github.com/nextcloud/server) license which is AGPLv3.
See [this article](https://www.fsf.org/bulletin/2021/fall/the-fundamentals-of-the-agplv3) from the FSF that explains what this license adds to the GPL one.

View file

@ -1,16 +0,0 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Recipes {#recipes}
This section of the manual gives you easy to follow recipes for common use cases.
```{=include=} chapters html:into-file=//recipes-dnsServer.html
recipes/dnsServer.md
```
```{=include=} chapters html:into-file=//recipes-exposeService.html
recipes/exposeService.md
```
```{=include=} chapters html:into-file=//recipes-serveStaticPages.html
recipes/serveStaticPages.md
```

View file

@ -1,186 +0,0 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Self-Host a DNS server {#recipes-dnsServer}
This recipe will show how to setup [dnsmasq][] as a local DNS server
that forwards all queries to your own domain `example.com` to a local IP - your server running SelfHostBlocks for example.
[dnsmasq]: https://dnsmasq.org/doc.html
Other DNS queries will be forwarded to an external DNS server
using [DNSSEC][] to encrypt your queries.
[DNSSEC]: https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions
For this to work, you must configure the DHCP server of your network
to set the DNS server to the IP of the host where the DNS server is running.
Usually, your ISP's router can do this but probably easier is to disable completely that DHCP server
and also self-host the DHCP server.
This recipe shows how to do that too.
## Why {#recipes-dnsServer-why}
_You want to hide your DNS queries from your ISP or other prying eyes._
Even if you use HTTPS to access an URL,
DNS queries are by default made in plain text.
Crazy, right?
So, even if the actual communication is encrypted,
everyone can see which site you're trying to access.
Using DNSSEC means encrypting the traffic to your preferred external DNS server.
Of course, that server will see what domain names you're trying to resolve,
but at least intermediary hops will not be able to anymore.
_You want more control on which DNS queries can be made._
Self-hosting your own DNS server means you can block some domains or subdomains.
This is done in practice by instructing your DNS server
to fail resolving some domains or subdomains.
Want to block Facebook for every host in the house?
That's the way to go.
Some routers allow this level of fine-tuning but if not,
self-hosting your own DNS server is the way to go.
## Drawbacks {#recipes-dnsServer-drawbacks}
Although it has some nice advantages,
self-hosting your own DNS server has one major drawback:
if it goes down, the whole household will be impacted.
By experience, it takes up to 5 minutes for others to notice something is wrong with internet.
So be wary when you deploy a new config.
## Recipe {#recipes-dnsServer-recipe}
The following snippet:
- Opens UDP port 53 in the firewall which is the ubiquitous (and hardcoded, crazy I know) port for DNS queries.
- Disables the default DNS resolver.
- Sets up dnsmasq as the DNS server.
- Optionally sets up dnsmasq as the DHCP server.
- Answers all DNS requests to your domain with the internal IP of the server.
- Forwards all other DNS requests to an external DNS server using DNSSEC.
This is done using [stubby][].
[stubby]: https://dnsprivacy.org/dns_privacy_daemon_-_stubby/
For more information about options, read the dnsmasq [manual][].
[manual]: https://dnsmasq.org/docs/dnsmasq-man.html
```nix
let
# Replace these values with what matches your network.
domain = "example.com";
serverIP = "192.168.1.30";
# This port is used internally for dnsmasq to talk to stubby on the loopback interface.
# Only change this if that port is already taken.
stubbyPort = 53000;
in
{
networking.firewall.allowedUDPPorts = [ 53 ];
services.resolved.enable = false;
services.dnsmasq = {
enable = true;
settings = {
inherit domain;
# Redirect queries to the stubby instance.
server = [
"127.0.0.1#${stubbyPort}"
"::1#${stubbyPort}"
];
# We do trust our own instance of stubby
# so we can proxy DNSSEC stuff.
# I'm not sure how useful this is.
proxy-dnssec = true;
# Log all queries.
# This produces a lot of log lines
# and looking at those can be scary!
log-queries = true;
# Do not look at /etc/resolv.conf
no-resolv = true;
# Do not forward externally reverse DNS lookups for internal IPs.
bogus-priv = true;
address = [
"/.${domain}/${serverIP}"
# You can redirect anything anywhere too.
"/pikvm.${domain}/192.168.1.31"
];
};
};
services.stubby = {
enable = true;
# It's a bit weird but default values comes from the examples settings hosted at
# https://github.com/getdnsapi/stubby/blob/develop/stubby.yml.example
settings = pkgs.stubby.passthru.settingsExample // {
listen_addresses = [
"127.0.0.1@${stubbyPort}"
"0::1@${stubbyPort}"
];
# For more example of good DNS resolvers,
# head to https://dnsprivacy.org/public_resolvers/
#
# The digest comes from https://nixos.wiki/wiki/Encrypted_DNS#Stubby
upstream_recursive_servers = [
{
address_data = "9.9.9.9";
tls_auth_name = "dns.quad9.net";
tls_pubkey_pinset = [
{
digest = "sha256";
value = "i2kObfz0qIKCGNWt7MjBUeSrh0Dyjb0/zWINImZES+I=";
}
];
}
{
address_data = "149.112.112.112";
tls_auth_name = "dns.quad9.net";
tls_pubkey_pinset = [
{
digest = "sha256";
value = "i2kObfz0qIKCGNWt7MjBUeSrh0Dyjb0/zWINImZES+I=";
}
];
}
];
};
};
}
```
Optionally, to use dnsmasq as the DHCP server too,
use the following snippet:
```nix
services.dnsmasq = {
settings = {
# When switching DNS server, accept old leases from previous server.
dhcp-authoritative = true;
# Adapt to your needs
# <ip-from>,<ip-to>,<mask>,<lease-ttl>
dhcp-range = "192.168.1.101,192.168.1.150,255.255.255.0,6h";
# Static DNS leases if needed.
# Choose an IP outside of the DHCP range
# <mac-address>,<DNS name>,<ip>,<lease-ttl>
dhcp-host = [
"12:34:56:78:9a:bc,server,192.168.1.50,infinite"
];
# Set default route to the router that can acccess the internet.
dhcp-option = [
"3,192.168.1.1"
];
};
};
```

View file

@ -1,156 +0,0 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Expose a service {#recipes-exposeService}
Let's see how one can use most of the blocks provided by SelfHostBlocks to make a service
accessible through a reverse proxy with LDAP and SSO integration as well as backing up
this service and creating a ZFS dataset to store the service's data.
We'll use an hypothetical well made service found under `services.awesome` as our example.
We're purposely not using a real service to avoid needing to deal with uninteresting particularities.
## Service setup {#recipes-exposeService-service}
Let's say our domain name is `example.com`,
and we want to reach our service under the `awesome` subdomain:
```nix
let
domain = "example.com";
subdomain = "awesome";
fqdn = "${subdomain}.${domain}";
listenPort = 9000;
dataDir = "/var/lib/awesome";
ldapGroup = "awesome_user";
in
```
We then `enable` the service and explicitly set the `listenPort` and `dataDir`,
assuming those options exist:
```nix
services.awesome = {
enable = true;
inherit dataDir listenPort;
};
```
## SSL Certificate {#recipes-exposeService-ssl}
Requesting an SSL certificate from Let's Encrypt is done by adding an entry to
the `extraDomains` option:
```nix
shb.certs.certs.letsencrypt.${domain}.extraDomains = [ fqdn ];
```
This assumes the `shb.certs` block has been configured:
```nix
shb.certs.certs.letsencrypt.${domain} = {
inherit domain;
group = "nginx";
reloadServices = [ "nginx.service" ];
adminEmail = "admin@${domain}";
};
```
## LDAP group {#recipes-exposeService-ldap}
We want only users of the group `calibre_user` to be able to access this subdomain.
The following snippet creates the LDAP group:
```nix
shb.lldap.ensureGroups = {
calibre_user = {};
};
```
## Reverse Proxy with Forward Auth {#recipes-exposeService-nginx}
If our service does not integrate with OIDC, we can still protect it with SSO
with forward authentication by letting the reverse proxy handle authentication.
This is done by adding an entry to `shb.nginx.vhosts`:
```nix
shb.nginx.vhosts = [
{
inherit subdomain domain;
ssl = config.shb.certs.certs.letsencrypt.${domain};
upstream = "http://127.0.0.1:${toString listenPort}";
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
autheliaRules = [{
policy = "one_factor";
subject = [ "group:${ldapGroup}" ];
}];
}
];
```
## ZFS support {#recipes-exposeService-zfs}
If you use ZFS, you can use SelfHostBlocks to create a dataset for you:
```nix
shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir;
```
## Debugging {#recipes-exposeService-debug}
Usually, the log level of the service can be increased with some option they provide.
With SelfHostBlocks, you can also introspect any HTTP service by adding an
`mitmdump` instance between the reverse proxy and the `awesome` service:
```nix
shb.mitmdump.awesome = {
inherit listenPort;
upstreamPort = listenPort + 1;
};
services.awesome.listenPort = lib.mkForce (listenPort + 1);
```
This creates a `mitmdump-awesome.service` systemd service which prints the requests' and responses' headers and bodies.
## Backup {#recipes-exposeService-backup}
The following snippet uses the `shb.restic` block to backup the `services.awesome.dataDir` directory:
```nix
shb.restic.instances.awesome = {
request.user = "awesome";
request.sourceDirectories = [ dataDir ];
settings.enable = true;
settings.passphrase.result = config.shb.sops.secret.awesome.result;
settings.repository.path = config.services.awesome.dataDir;
};
shb.sops.secret."awesome" = {
request = config.shb.restic.instances.awesome.settings.passphrase.request;
};
```
## Impermanence {#recipes-exposeService-impermanence}
To save the data folder in an impermanence setup, add:
```nix
{
shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir;
}
```
## Application Dashboard {#recipes-exposeService-applicationdashboard}
For example using the [Homepage](services-homepage.html) service:
```nix
{
shb.homepage.servicesGroups.MyServices.services.Awesome = {
sortOrder = 1;
dashboard.request = {
externalUrl = "https://${fqdn}";
internalUrl = "http://127.0.0.1:${toString listenPort}";
};
};
}
```

View file

@ -1,75 +0,0 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Serve Static Pages {#recipes-serveStaticPages}
This recipe shows how to use SelfHostBlocks blocks to serve static web pages using the Nginx reverse proxy with SSL termination.
In this recipe, we'll assume the pages to serve are found under the `/srv/my-website` path and will be served under the `my-website.example.com` fqdn.
```nix
let
name = "my-website";
subdomain = name;
domain = "example.com";
fqdn = "${subdomain}.${domain}";
user = "me";
in
```
We also assume the static web pages are owned and updated by the user named `me`.
## ZFS dataset {#recipes-serveStaticPages-zfs}
We can create a ZFS dataset with:
```nix
shb.zfs.datasets."safe/${name}".path = "/srv/${name}";
```
## SSL Certificate {#recipes-serveStaticPages-ssl}
Requesting an SSL certificate from Let's Encrypt is done by adding an entry to
the `extraDomains` option:
```nix
shb.certs.certs.letsencrypt.${domain}.extraDomains = [ fqdn ];
```
This assumes the `shb.certs` block has been configured:
```nix
shb.certs.certs.letsencrypt.${domain} = {
inherit domain;
group = "nginx";
reloadServices = [ "nginx.service" ];
adminEmail = "admin@${domain}";
};
```
## Reverse Proxy {#recipes-serveStaticPages-nginx}
First, we make the parent directory owned by the user which will upload them and `nginx`:
```nix
systemd.tmpfiles.rules = lib.mkBefore [
"d '/srv/${name}' 0750 ${user} nginx - -"
];
```
Now, we can setup nginx. The following snippet serves files from the `/srv/${name}/` directory.
```nix
services.nginx.enable = true;
services.nginx.virtualHosts."skarabox.${domain}" = {
forceSSL = true;
sslCertificate = config.shb.certs.certs.letsencrypt."${domain}".paths.cert;
sslCertificateKey = config.shb.certs.certs.letsencrypt."${domain}".paths.key;
locations."/" = {
root = "/srv/${name}/";
extraConfig = ''
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header Cache-Control "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400, must-revalidate, public";
'';
};
};
```

File diff suppressed because it is too large Load diff

View file

@ -1,629 +0,0 @@
# SelfHostBlocks Service Implementation Guide {#service-implementation-guide}
This guide documents the complete process for implementing a new service in SelfHostBlocks, based on lessons learned from the nzbget implementation and analysis of existing service patterns.
**Note**: SelfHostBlocks aims to be "the smallest amount of code above what is available in nixpkgs" (see `docs/contributing.md`). Services should leverage existing nixpkgs options when possible and focus on providing contract integrations rather than reimplementing configuration.
## What Makes a "Complete" SHB Service {#complete-shb-service}
According to the project maintainer's criteria, a service is considered fully supported if it includes:
1. **SSL block integration** - HTTPS/TLS certificate management
2. **Backup block integration** - Automated backup of service data
3. **Monitoring integration** - Prometheus metrics and health checks
4. **LDAP (LLDAP) integration** - Directory-based authentication
5. **SSO (Authelia) integration** - Single sign-on authentication
6. **Comprehensive tests** - All integration variants tested
## Pre-Implementation Research {#pre-implementation-research}
### 1. Analyze Existing Services {#analyze-existing-services}
Before starting, study existing services to understand patterns:
```bash
# Study service patterns
ls modules/services/ # List all services
cat modules/services/deluge.nix # Best practice example
cat modules/services/vaultwarden.nix # Another good example
```
**Key patterns to identify:**
- Configuration structure and options
- How contracts are used (SSL, backup, monitoring, secrets)
- Authentication integration approaches
- Service-specific settings and defaults
### 2. Understand the Target Service {#understand-target-service}
Research the service you're implementing:
- **Configuration format** (YAML, INI, JSON, etc.)
- **Authentication methods** (built-in users, LDAP, OIDC/OAuth)
- **API endpoints** (for monitoring/health checks)
- **Data directories** (what needs backing up)
- **Network requirements** (ports, protocols)
- **Dependencies** (databases, external tools)
### 3. Check NixOS Integration {#check-nixos-integration}
Verify nixpkgs support:
```bash
# Check if NixOS service exists
nix eval --impure --expr '(import <nixpkgs/nixos> { configuration = {...}: {}; }).options.services' --apply 'builtins.attrNames' --json | jq -r '.[]' | grep -i servicename
# or search online: https://search.nixos.org/options?query=services.servicename
```
If no nixpkgs integration exists, you may need to:
- Package the service first
- Use containerized approach
- Request upstream nixpkgs integration
## Implementation Steps {#implementation-steps}
### 1. Create the Service Module {#create-service-module}
Location: `modules/services/servicename.nix`
**Basic structure:**
```nix
{ config, pkgs, lib, ... }:
let
cfg = config.shb.servicename;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}";
# Choose appropriate format based on service config
settingsFormat = pkgs.formats.yaml {}; # or .ini, .json, etc.
in
{
options.shb.servicename = {
# Core options (always required)
enable = lib.mkEnableOption "selfhostblocks.servicename";
subdomain = lib.mkOption { ... };
domain = lib.mkOption { ... };
# SSL integration (always include)
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
# Service-specific options
port = lib.mkOption { ... };
dataDir = lib.mkOption { ... };
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
# Define key options with descriptions
};
};
};
# Authentication options
authEndpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "OIDC endpoint for SSO";
default = null;
};
ldap = lib.mkOption { ... }; # LDAP integration
users = lib.mkOption { ... }; # Local user management
# Integration options
backup = lib.mkOption {
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "servicename";
sourceDirectories = [ cfg.dataDir ];
};
};
};
monitoring = lib.mkOption {
type = lib.types.nullOr (lib.types.submodule {
options = {
# Service-specific monitoring options
};
});
default = null;
};
# System options
extraServiceConfig = lib.mkOption { ... };
logLevel = lib.mkOption { ... };
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
# Base service configuration
services.servicename = {
enable = true;
# Map SHB options to nixpkgs service options
};
# Nginx reverse proxy
shb.nginx.vhosts = [{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString cfg.port}";
# SSO integration
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
{
domain = fqdn;
policy = "bypass";
resources = [ "^/api" ]; # API endpoints
}
{
domain = fqdn;
policy = "two_factor";
resources = [ "^.*" ]; # Everything else
}
];
}];
# User/group setup
users.users.servicename = {
extraGroups = [ "media" ]; # If needed for file access
};
# Directory permissions
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 servicename servicename - -"
];
}
# Monitoring configuration (conditional)
(lib.mkIf (cfg.monitoring != null) {
services.prometheus.scrapeConfigs = [{
job_name = "servicename";
static_configs = [{
targets = [ "127.0.0.1:${toString cfg.port}" ];
labels = {
hostname = config.networking.hostName;
domain = cfg.domain;
};
}];
metrics_path = "/metrics"; # or appropriate endpoint
scrape_interval = "30s";
}];
})
]);
}
```
### 2. Key Implementation Considerations {#implementation-considerations}
#### Configuration Management {#configuration-management}
- **Use freeform settings** when possible: `freeformType = settingsFormat.type`
- **Provide sensible defaults** for common options
- **Use lib.mkDefault** for user-overridable settings
- **Use lib.mkForce** for security-critical settings
#### Authentication Integration {#authentication-integration}
- **SSO (Authelia)**: Use `autheliaRules` with appropriate bypass policies
- **LDAP**: Follow the patterns from existing services
- **Local users**: Use SHB secret contracts for password management
#### Security Best Practices {#security-best-practices}
- **Bind to localhost**: Services should listen on `127.0.0.1` only
- **Use nginx for TLS**: Don't configure TLS in the service itself
- **Proper file permissions**: Use systemd.tmpfiles.rules
- **Secret management**: Always use SHB secret contracts
### 3. Monitoring Implementation {#monitoring-implementation}
Choose the appropriate monitoring approach:
#### Option A: Native Prometheus Metrics {#native-prometheus-metrics}
If the service supports Prometheus natively:
```nix
services.prometheus.scrapeConfigs = [{
job_name = "servicename";
static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }];
metrics_path = "/metrics";
}];
```
#### Option B: API Health Check {#api-health-check}
If no native metrics, monitor API endpoints:
```nix
services.prometheus.scrapeConfigs = [{
job_name = "servicename";
static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }];
metrics_path = "/api/status"; # or appropriate endpoint
}];
```
#### Option C: External Exporter {#external-exporter}
For services requiring dedicated exporters (like Deluge):
```nix
services.prometheus.exporters.servicename = {
enable = true;
# exporter-specific configuration
};
```
### 4. Create Comprehensive Tests {#create-comprehensive-tests}
Location: `test/services/servicename.nix`
**Test structure:**
```nix
{ pkgs, ... }:
let
testLib = pkgs.callPackage ../common.nix {};
# Common test scripts
commonTestScript = testLib.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.servicename.ssl);
waitForServices = { ... }: [ "nginx.service" "servicename.service" ];
waitForPorts = { node, ... }: [ node.config.services.servicename.port ];
# Service-specific connectivity test
extraScript = { node, proto_fqdn, ... }: ''
with subtest("service connectivity"):
response = curl(client, "", "${proto_fqdn}/api/health")
# Add service-specific checks
'';
};
# Monitoring test script
prometheusTestScript = { nodes, ... }: ''
server.wait_for_open_port(${toString nodes.server.config.services.servicename.port})
with subtest("prometheus monitoring"):
# Test the actual monitoring endpoint
response = server.succeed("curl -sSf http://localhost:${port}/metrics")
# Validate response format
'';
# Base configuration
basic = { config, ... }: {
imports = [
testLib.baseModule
../../modules/services/servicename.nix
];
shb.servicename = {
enable = true;
inherit (config.test) domain subdomain;
# Basic configuration
};
};
in {
# Test variants (all 6 required)
basic = lib.shb.test.runNixOSTest { ... };
backup = lib.shb.test.runNixOSTest { ... };
https = lib.shb.test.runNixOSTest { ... };
ldap = lib.shb.test.runNixOSTest { ... };
monitoring = lib.shb.test.runNixOSTest { ... };
sso = lib.shb.test.runNixOSTest { ... };
}
```
#### Required Test Variants {#required-test-variants}
1. **basic**: Core functionality without authentication
2. **backup**: Tests backup integration
3. **https**: Tests SSL/TLS integration
4. **ldap**: Tests LDAP authentication
5. **monitoring**: Tests Prometheus integration
6. **sso**: Tests Authelia SSO integration
### 5. Update Flake Configuration {#update-flake-configuration}
Add to `flake.nix`:
```nix
allModules = [
# ... existing modules
modules/services/servicename.nix
];
```
```nix
checks = {
# ... existing checks
// (vm_test "servicename" ./test/services/servicename.nix)
};
```
### 6. Create Service Documentation {#create-service-documentation}
Create comprehensive documentation for the new service:
**Location**: `modules/services/servicename/docs/default.md`
```markdown
# ServiceName Service {\#services-servicename}
Brief description of what the service does.
## Features {\#services-servicename-features}
- Feature 1
- Feature 2
## Usage {\#services-servicename-usage}
### Basic Configuration {\#services-servicename-basic}
shb.servicename = {
enable = true;
domain = "example.com";
subdomain = "servicename";
};
### SSL Configuration {\#services-servicename-ssl}
shb.servicename.ssl.paths = {
cert = /path/to/cert;
key = /path/to/key;
};
## Options Reference {\#services-servicename-options}
{=include=} options
id-prefix: services-servicename-options-
list-id: selfhostblocks-servicename-options
source: @OPTIONS_JSON@
```
**Important**: Use consistent heading ID patterns:
- Service overview: `{\#services-servicename}`
- Features: `{\#services-servicename-features}`
- Usage sections: `{\#services-servicename-basic}`, `{\#services-servicename-ssl}`, etc.
- Options: `{\#services-servicename-options}`
Note: Replace `servicename` with your actual service name (e.g., `nzbget`, `jellyfin`).
For the `@OPTIONS_JSON@` to work, a line must be added
in the `flake.nix` file:
```nix
packages.manualHtml = pkgs.callPackage ./docs {
modules = {
"blocks/authelia" = ./modules/blocks/authelia.nix;
// Add line and keep in alphabetical order.
};
};
```
### 7. Update Redirects Automatically {#update-redirects-automatically}
After creating documentation, generate the required redirects:
```bash
# Scan documentation and add missing redirects
nix run .#update-redirects
# Review the changes
git diff docs/redirects.json
# The tool will show what redirects were added
```
The automation will:
- Find all heading IDs in your documentation
- Generate appropriate redirect entries
- Add them to `docs/redirects.json`
- Follow established naming patterns
### 8. Handle Unfree Dependencies {#handle-unfree-dependencies}
If the service requires unfree packages:
```nix
# In flake.nix
config = {
allowUnfree = true;
permittedInsecurePackages = [
# List any required insecure packages
];
};
```
Update CI workflow if needed:
```yaml
# In .github/workflows/build.yaml
- name: Setup Nix
uses: cachix/install-nix-action@v31
with:
extra_nix_config: |
allow-unfree = true
```
## Testing and Validation {#testing-and-validation}
### Local Testing {#local-testing}
```bash
# Test redirect automation
nix run .#update-redirects
# Test all service variants (replace ${system} with your system, e.g., x86_64-linux)
nix build .#checks.${system}.vm_servicename_basic
nix build .#checks.${system}.vm_servicename_backup
nix build .#checks.${system}.vm_servicename_https
nix build .#checks.${system}.vm_servicename_ldap
nix build .#checks.${system}.vm_servicename_monitoring
nix build .#checks.${system}.vm_servicename_sso
# Or run all tests (as recommended in docs/contributing.md)
nix flake check
# For interactive testing and debugging, see docs/contributing.md:
# nix run .#checks.${system}.vm_servicename_basic.driverInteractive
# Test documentation build (includes redirect validation)
nix build .#manualHtml
```
To continuously rebuild the documentation of file change, run the following command.
To exit, you'll need to do Ctrl-C twice in a row.
```bash
nix run .#manualHtml-watch
```
### Iterative Development Approach {#iterative-development-approach}
1. **Start with basic functionality** - get core service working
2. **Add SSL integration** - enable HTTPS
3. **Add backup integration** - ensure data protection
4. **Add monitoring** - implement health checks
5. **Add authentication** - LDAP and SSO integration
6. **Create documentation** - write service documentation with heading IDs
7. **Update redirects** - run `nix run .#update-redirects` to generate redirects
8. **Comprehensive testing** - all 6 test variants
9. **Final validation** - ensure documentation builds correctly
## Common Pitfalls and Solutions {#common-pitfalls-and-solutions}
### Configuration Issues {#configuration-issues}
- **Problem**: Service doesn't start due to config validation
- **Solution**: Use `lib.mkDefault` for user settings, `lib.mkForce` for security settings
### Authentication Integration {#authentication-integration-pitfalls}
- **Problem**: SSO redirect loops or access denied
- **Solution**: Check `autheliaRules` bypass patterns for API endpoints
### Monitoring Failures {#monitoring-failures}
- **Problem**: Prometheus scraping fails with 404
- **Solution**: Verify the actual API endpoints the service provides
### Test Failures {#test-failures}
- **Problem**: VM tests timeout or fail connectivity
- **Solution**: Check `waitForServices` and `waitForPorts` configurations
### Nixpkgs Integration {#nixpkgs-integration}
- **Problem**: Service options don't match SHB needs
- **Solution**: Map SHB options to nixpkgs options, use `extraConfig` for overrides
## Best Practices Summary {#best-practices-summary}
1. **Follow existing patterns** - study deluge.nix and vaultwarden.nix
2. **Use freeform configuration** - maximum flexibility with typed key options
3. **Implement all contracts** - SSL, backup, monitoring, secrets
4. **Test comprehensively** - all 6 integration variants
5. **Security first** - localhost binding, proper permissions, secret management
6. **Document thoroughly** - clear descriptions for all options
7. **Iterative development** - build complexity gradually
8. **CI/CD validation** - ensure all tests pass before submission
## Redirect Management {#redirect-management}
SelfHostBlocks uses `nixos-render-docs` for documentation generation, which includes built-in redirect validation. The `docs/redirects.json` file maps documentation identifiers to their target URLs.
### Automated Redirect Generation {#automated-redirect-generation}
SelfHostBlocks includes an automated redirect management tool that leverages the official `nixos-render-docs` ecosystem:
```bash
# Generate fresh redirects from HTML documentation
nix run .#update-redirects
```
This tool:
- **Generates HTML documentation** using `nixos-render-docs` with redirect collection enabled
- **Scans actual HTML files** for anchor IDs to ensure perfect accuracy
- **Creates fresh redirects** from scratch by mapping anchors to their real file locations
- **Filters system-generated anchors** (excludes `opt-*` and `selfhostblock*` entries)
- **Provides interactive confirmation** before updating `docs/redirects.json`
### How Redirects Work {#how-redirects-work}
1. **nixos-render-docs validation**: During documentation builds, `nixos-render-docs` automatically validates that all heading IDs have corresponding redirect entries
2. **Automated maintenance**: The `update-redirects` tool automatically maintains `redirects.json` by:
- Building HTML documentation with patched `nixos-render-docs`
- Scanning generated HTML files for actual anchor IDs and their file locations
- Creating accurate redirect mappings without guesswork or pattern matching
3. **Manual override**: You can still manually edit `docs/redirects.json` for special cases
### Redirect Patterns {#redirect-patterns}
The automation follows these patterns when mapping headings to redirect targets:
| Heading ID | Source File | Redirect Target |
|------------|-------------|-----------------|
| `services-nzbget-basic` | `modules/services/nzbget/docs/default.md` | `["services-nzbget.html#services-nzbget-basic"]` |
| `blocks-monitoring` | `modules/blocks/monitoring/docs/default.md` | `["blocks-monitoring.html#blocks-monitoring"]` |
| `demo-nextcloud` | `demo/nextcloud/README.md` | `["demo-nextcloud.html#demo-nextcloud"]` |
| `contracts` | `docs/contracts.md` | `["contracts.html#contracts"]` |
Note: Redirects always include the anchor link (`#heading-id`) to jump to the specific heading within the target page.
### Adding New Service Documentation {#adding-new-service-documentation}
When implementing a new service, the redirect workflow is now automated:
1. **Write documentation** with heading IDs:
```markdown
# NewService {\#services-newservice}
## Basic Configuration {\#services-newservice-basic}
```
2. **Update redirects automatically**:
```bash
nix run .#update-redirects
```
3. **Review and commit** the changes:
```bash
git add docs/redirects.json modules/services/newservice/docs/default.md
git commit -m "Add newservice documentation"
```
### Build-time Validation {#build-time-validation}
The documentation build process will fail if:
- Any documentation heading ID lacks a corresponding redirect entry
- Redirect targets point to non-existent content
- There are formatting errors in the redirects file
This ensures documentation links remain functional when content is moved or reorganized.
## Resources {#resources}
- **Contributing guide**: `docs/contributing.md` for authoritative development workflows and testing procedures
- **Existing services**: `modules/services/` for patterns and implementation examples
- **Contracts documentation**: `modules/contracts/` for understanding integration interfaces
- **Test framework**: `test/common.nix` for testing utilities and patterns
- **NixOS options**: https://search.nixos.org/options for upstream service options
- **SHB documentation**: Generated docs showing existing service patterns
- **Redirect automation**: `nix run .#update-redirects` for automated redirect management
- **nixos-render-docs**: Built-in redirect validation and documentation generation
## Quick Reference {#quick-reference}
### Complete Workflow {#complete-workflow}
```bash
# 1. Implement service module
vim modules/services/SERVICENAME.nix
# 2. Create tests
vim test/services/SERVICENAME.nix
# 3. Update flake
vim flake.nix # Add to allModules and checks
# 4. Write documentation
vim modules/services/SERVICENAME/docs/default.md
# 5. Generate redirects
nix run .#update-redirects
# 6. Test everything
nix flake check # Run all tests (recommended)
# Or test specific variants:
# nix build .#checks.${system}.vm_SERVICENAME_basic
nix build .#manualHtml
# 7. Commit changes
git add .
git commit -m "Add SERVICENAME with full integration"
```
This guide provides a complete roadmap for implementing production-ready SelfHostBlocks services that meet the project's quality standards.

View file

@ -1,10 +1,7 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Services {#services} # Services {#services}
Services are usually web applications that SHB help you self-host some of your data. Services are usually web applications that SHB help you self-host. Configuration of those is
Configuration of those is purposely made more opinionated than the upstream nixpkgs modules purposely made opinionated and require as few nix options as can make sense. That is possible thanks to the extensive use of blocks provided by SHB.
in exchange for an uniformized configuration experience.
That is possible thanks to the extensive use of blocks provided by SHB.
::: {.note} ::: {.note}
Not all services are yet documented. You can find all available services [in the repository](@REPO@/modules/services). Not all services are yet documented. You can find all available services [in the repository](@REPO@/modules/services).
@ -14,104 +11,16 @@ The following table summarizes for each documented service what features it prov
information is provided in the respective manual sections. information is provided in the respective manual sections.
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling | | Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|-----------------------------|--------|---------------|-----|-------|------------|-----------| |-----------------------|--------|---------------|-----|-------|------------|-----------|
| [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N | | [Nextcloud Server][1] | P (1) | Y | Y | Y | Y | P (2) |
| [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
| [Homepage][] | Y (1) | Y | N | Y | Y (2) | N |
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
| [Open WebUI][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Pinchflat][] | Y | Y | Y | Y (4) | Y (5) | N |
| [Simple NixOS Mailserver][] | Y | Y | N | Y | Y | N |
| [Vaultwarden][] | Y (1) | Y | Y | Y | Y (2) | N |
Legend: **N**: no but WIP; **P**: partial; **Y**: yes Legend: **N**: no but WIP; **P**: partial; **Y**: yes
1. Database and data files are backed up separately. 1. Does not backup the database yet.
This could lead to backups not being in sync. 2. Works but the traces are not exported to Grafana yet.
Any idea on how to fix this is welcomed!
2. Dashboard is common to all services.
3. Works but the traces are not exported to Grafana yet.
4. Uses LDAP indirectly through forward auth.
[*Arr]: services-arr.html [1]: services-nextcloud.html
[Firefly-iii]: services-firefly-iii.html
[Forgejo]: services-forgejo.html
[Home-Assistant]: services-home-assistant.html
[Homepage]: services-homepage.html
[Jellyfin]: services-jellyfin.html
[Karakeep]: services-karakeep.html
[Nextcloud Server]: services-nextcloud.html
[Open WebUI]: services-open-webui.html
[Pinchflat]: services-pinchflat.html
[Simple NixOS Mailserver]: services-mailserver.html
[Vaultwarden]: services-vaultwarden.html
## Dashboard {#services-category-dashboard}
```{=include=} chapters html:into-file=//services-homepage.html
modules/services/homepage/docs/default.md
```
## Documents {#services-category-documents}
```{=include=} chapters html:into-file=//services-nextcloud.html ```{=include=} chapters html:into-file=//services-nextcloud.html
modules/services/nextcloud-server/docs/default.md modules/services/nextcloud-server/docs/default.md
``` ```
## Emails {#services-category-emails}
```{=include=} chapters html:into-file=//services-mailserver.html
modules/services/mailserver/docs/default.md
```
## Passwords {#services-category-passwords}
```{=include=} chapters html:into-file=//services-vaultwarden.html
modules/services/vaultwarden/docs/default.md
```
## Automation {#services-category-automation}
```{=include=} chapters html:into-file=//services-home-assistant.html
modules/services/home-assistant/docs/default.md
```
## AI {#services-category-ai}
```{=include=} chapters html:into-file=//services-karakeep.html
modules/services/karakeep/docs/default.md
```
```{=include=} chapters html:into-file=//services-open-webui.html
modules/services/open-webui/docs/default.md
```
## Code {#services-category-code}
```{=include=} chapters html:into-file=//services-forgejo.html
modules/services/forgejo/docs/default.md
```
## Media {#services-category-media}
```{=include=} chapters html:into-file=//services-arr.html
modules/services/arr/docs/default.md
```
```{=include=} chapters html:into-file=//services-jellyfin.html
modules/services/jellyfin/docs/default.md
```
```{=include=} chapters html:into-file=//services-pinchflat.html
modules/services/pinchflat/docs/default.md
```
## Finance {#services-category-finance}
```{=include=} chapters html:into-file=//services-firefly-iii.html
modules/services/firefly-iii/docs/default.md
```

View file

@ -1,138 +1,21 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Usage {#usage} # Usage {#usage}
## Flake {#usage-flake} ## Flake {#usage-flake}
Self Host Blocks (SHB) is available as a flake. It also uses its own `pkgs.lib` and Self Host Blocks is available as a flake. To use it in your project, add the following flake input:
`nixpkgs` and it is required to use the provided ones as input for your
deployments, otherwise you might end up blocked when SHB patches a
module, function or package. The following snippet is thus required to use Self
Host Blocks:
```nix ```nix
{
inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks"; inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks";
outputs = { selfhostblocks, ... }: let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
in
nixosConfigurations = {
myserver = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
./configuration.nix
];
};
};
}
``` ```
::: {.note} Then, in your `nixosConfigurations`, import the module with:
In case somehow this documentation became stale,
look at the examples in [`./demo/minimal/flake.nix`](@REPO@/demo/minimal/flake.nix)
which provides examples tested in CI - so assured to always be up to date -
on how to use SHB.
:::
### Modules {#usage-flake-modules} ```nix
imports = [
The `default` module imports all modules except the SOPS module. inputs.selfhostblocks.nixosModules.x86_64-linux.default
That module is only needed if you want to use [sops-nix](#usage-secrets) to manage secrets.
You can also import each module individually.
You might want to do this to only import SHB overlays if you actually intend to use them.
Importing the `nextcloud` module for example will anyway transitively import needed support modules
so you can't go wrong:
```diff
modules = [
- selfhostblocks.nixosModules.default
+ selfhostblocks.nixosModules.nextcloud
./configuration.nix
]; ];
``` ```
To list all modules, run:
```bash
$ nix flake show github:ibizaman/selfhostblocks --allow-import-from-derivation
...
├───nixosModules
│ ├───arr: NixOS module
│ ├───audiobookshelf: NixOS module
│ ├───authelia: NixOS module
│ ├───borgbackup: NixOS module
│ ├───davfs: NixOS module
│ ├───default: NixOS module
│ ├───deluge: NixOS module
│ ├───forgejo: NixOS module
│ ├───grocy: NixOS module
│ ├───hardcodedsecret: NixOS module
│ ├───hledger: NixOS module
│ ├───home-assistant: NixOS module
│ ├───immich: NixOS module
│ ├───jellyfin: NixOS module
│ ├───karakeep: NixOS module
│ ├───lib: NixOS module
│ ├───lldap: NixOS module
│ ├───mitmdump: NixOS module
│ ├───monitoring: NixOS module
│ ├───nextcloud-server: NixOS module
│ ├───nginx: NixOS module
│ ├───open-webui: NixOS module
│ ├───paperless: NixOS module
│ ├───pinchflat: NixOS module
│ ├───postgresql: NixOS module
│ ├───restic: NixOS module
│ ├───sops: NixOS module
│ ├───ssl: NixOS module
│ ├───tinyproxy: NixOS module
│ ├───vaultwarden: NixOS module
│ ├───vpn: NixOS module
│ └───zfs: NixOS module
...
```
### Patches {#usage-flake-patches}
To add your own patches on top of the patches provided by SHB,
you can remove the `patchedNixpkgs` line and instead apply the patches yourself:
```diff
- nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
+ pkgs = import selfhostblocks.inputs.nixpkgs {
+ inherit system;
+ };
+ nixpkgs' = pkgs.applyPatches {
+ name = "nixpkgs-patched";
+ src = selfhostblocks.inputs.nixpkgs;
+ patches = selfhostblocks.lib.${system}.patches;
+ };
+ nixosSystem' = import "${nixpkgs'}/nixos/lib/eval-config.nix";
in
nixosConfigurations = {
- myserver = nixpkgs'.nixosSystem {
+ myserver = nixosSystem' {
```
### Overlays {#usage-flake-overlays}
SHB applies its own overlays using `nixpkgs.overlays`.
Each module provided by SHB set that option if needed.
If you don't want to have those overlays applied for modules you don't intend to use SHB for,
you will want to avoid importing the `default` module
and instead import only the module for the services or blocks you intend to use,
like shows in the [Modules](#usage-flake-modules) section.
### Substituter {#usage-flake-substituter}
You can also use the public cache as a substituter with: You can also use the public cache as a substituter with:
```nix ```nix
@ -145,230 +28,49 @@ nix.settings.substituters = [
]; ];
``` ```
### Unfree {#usage-flake-unfree} For now, Self Host Blocks has a hard dependency on `sops-nix`. I am [working on removing
that](https://github.com/ibizaman/selfhostblocks/issues/24) so you can use any secrets manager you
SHB does not necessarily attempt to provide only free packages. want. Until then, you also need to import the `sops-nix` module:
Currently, the only module using unfree modules is the [Open WebUI](@REPO@/modules/services/open-webui.nix) one.
To be able to use that module, you can follow the [nixpkgs manual](https://nixos.org/manual/nixpkgs/stable/#sec-allow-unfree)
and set either:
```nix ```nix
{ imports = [
nixpkgs.config.allowUnfree = true; inputs.selfhostblocks.inputs.sops-nix.nixosModules.default
}
```
or the option `nixpkgs.config.allowUnfreePredicate`.
### Tag Updates {#usage-flake-tag}
To pin SHB to a release/tag, you can either use an implicit or explicit way.
#### Implicit {#usage-flake-tag-implicit}
Here, use the usual `inputs` form:
```nix
{
inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks";
}
```
then use the `flake update --override-input` command:
```bash
nix flake update selfhostblocks \
--override-input selfhostblocks github:ibizaman/selfhostblocks/@VERSION@
```
Note that running `nix flake update` will update the version of SHB to the latest from the main branch,
canceling the override you just did above.
So beware when running that command.
#### Explicit {#usage-flake-tag-explicit}
Here, set the version in the input directly:
```nix
{
inputs.selfhostblocks.url = "github:ibizaman/selfhostblocks?ref=@VERSION@";
}
```
Note that running `nix flake update` in this case will not update SHB,
you must update the tag explicitly then run `nix flake update`.
### Auto Updates {#usage-flake-autoupdate}
To avoid burden on the maintainers to keep `nixpkgs` input updated with
upstream, the [GitHub repository][repo] for SHB updates the
`nixpkgs` input every couple days, and verifies all tests pass before
automatically merging the new `nixpkgs` version. The setup is explained in
[this blog post][automerge].
[repo]: https://github.com/ibizaman/selfhostblocks
[automerge]: https://blog.tiserbox.com/posts/2023-12-25-automated-flake-lock-update-pull-requests-and-merging.html
### Lib {#usage-flake-lib}
The `selfhostblocks.nixosModules.lib` module
adds a module argument called `shb` by setting the
`_module.args.shb` option.
It is imported by nearly all other SHB modules
but you could still import it on its own
if you want to access SHB's functions and no other module.
The library of functions is also available under the traditional
`selfhostblocks.lib` flake output.
The functions layout is, in pseudo-code:
- `shb.*` all functions from [`./lib/default.nix`](@REPO@/lib/default.nix).
- `shb.contracts.*` all functions from [`./modules/contracts/default.nix`](@REPO@/modules/contracts/default.nix).
- `shb.test.*` all functions from [`./test/common.nix`](@REPO@/test/common.nix).
## Example Deployments {#usage-examples}
### With Nixos-Rebuild {#usage-examples-nixosrebuild}
The following snippets show how to deploy SHB using the standard
deployment system [nixos-rebuild][nixos-rebuild].
[nixos-rebuild]: https://nixos.org/manual/nixos/stable/#sec-changing-config
```nix
{
inputs = {
selfhostblocks.url = "github:ibizaman/selfhostblocks";
};
outputs = { self, selfhostblocks }: let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
in {
nixosConfigurations = {
machine = nixpkgs'.nixosSystem {
inherit system;
modules = [
selfhostblocks.nixosModules.default
]; ];
};
};
};
}
``` ```
The above snippet assumes one machine to deploy to, so `nixpkgs` is defined Self Host Blocks provides its own `nixpkgs` input so both can be updated in lock step, ensuring
exclusively by the `selfhostblocks` input. It is more likely that you have maximum compatibility. It is recommended to use the following `nixpkgs` as input for your deployments:
multiple machines, some not using SHB, then you can do the following:
```nix
inputs.selfhostblocks.inputs.nixpkgs
```
Advanced users can if they wish use a version of `nixpkgs` of their choosing but then we cannot
guarantee Self Host Block won't use a non-existing option from `nixpkgs`.
To avoid manually updating the `nixpkgs` version, the [GitHub repository][1] for Self Host Blocks
tries to update the `nixpkgs` input daily, verifying all tests pass before accepting this new
`nixpkgs` version. The setup is explained in [this blog post][2].
[1]: https://github.com/ibizaman/selfhostblocks
[2]: https://blog.tiserbox.com/posts/2023-12-25-automated-flake-lock-update-pull-requests-and-merging.html
## Example Deployment with Nixos-Rebuild {#usage-example-nixosrebuild}
The following snippets show how to deploy Self Host Blocks using the standard deployment system `nixos-rebuild`.
```nix ```nix
{ {
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
selfhostblocks.url = "github:ibizaman/selfhostblocks"; selfhostblocks.url = "github:ibizaman/selfhostblocks";
}; };
outputs = { self, selfhostblocks }: { outputs = { self, selfhostblocks }: {
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
in
nixosConfigurations = { nixosConfigurations = {
machine1 = nixpkgs.lib.nixosSystem { machine = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
};
machine2 = nixpkgs'.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
selfhostblocks.nixosModules.default selfhostblocks.nixosModules.${system}.default
];
};
};
};
}
```
In the above snippet, `machine1` will use the `nixpkgs` version from your inputs
while `machine2` will use the `nixpkgs` version from `selfhostblocks`.
### With Colmena {#usage-examples-colmena}
The following snippets show how to deploy SHB using the deployment
system [Colmena][Colmena].
[colmena]: https://colmena.cli.rs
```nix
{
inputs = {
selfhostblocks.url = "github:ibizaman/selfhostblocks";
};
outputs = { self, selfhostblocks }: {
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
pkgs' = import nixpkgs' {
inherit system;
};
in
colmena = {
meta = {
nixpkgs = pkgs';
};
machine = { selfhostblocks, ... }: {
imports = [
selfhostblocks.nixosModules.default
];
};
};
};
}
```
The above snippet assumes one machine to deploy to, so `nixpkgs` is defined
exclusively by the `selfhostblocks` input. It is more likely that you have
multiple machines, some not using SHB, in this case you can use the
`colmena.meta.nodeNixpkgs` option:
```nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
selfhostblocks.url = "github:ibizaman/selfhostblocks";
};
outputs = { self, selfhostblocks }: {
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
};
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
pkgs' = import nixpkgs' {
inherit system;
};
in
colmena = {
meta = {
nixpkgs = pkgs;
nodeNixpkgs = {
machine2 = pkgs';
};
};
machine1 = ...;
machine2 = { selfhostblocks, ... }: {
imports = [
selfhostblocks.nixosModules.default
]; ];
# Machine specific configuration goes here. # Machine specific configuration goes here.
@ -378,15 +80,9 @@ multiple machines, some not using SHB, in this case you can use the
} }
``` ```
In the above snippet, `machine1` will use the `nixpkgs` version from your inputs The above snippet is very minimal as it assumes you have only one machine to deploy to, so `nixpkgs`
while `machine2` will use the `nixpkgs` version from `selfhostblocks`. is defined exclusively by the `selfhostblocks.inputs.nixpkgs` input. If some machines are not using
Self Host Blocks, you can do the following:
### With Deploy-rs {#usage-examples-deploy-rs}
The following snippets show how to deploy SHB using the deployment
system [deploy-rs][deploy-rs].
[deploy-rs]: https://github.com/serokell/deploy-rs
```nix ```nix
{ {
@ -395,60 +91,28 @@ system [deploy-rs][deploy-rs].
}; };
outputs = { self, selfhostblocks }: { outputs = { self, selfhostblocks }: {
let nixosConfigurations = {
machine1 = nixpkgs.lib.nixosSystem {
};
machine2 = selfhostblocks.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
pkgs' = import nixpkgs' {
inherit system;
};
deployPkgs = import selfhostblocks.inputs.nixpkgs {
inherit system;
overlays = [
deploy-rs.overlay
(self: super: {
deploy-rs = {
inherit (pkgs') deploy-rs;
lib = super.deploy-rs.lib;
};
})
];
};
in
nixosModules.machine = {
imports = [
selfhostblocks.nixosModules.default
];
};
nixosConfigurations.machine = nixpkgs'.nixosSystem {
inherit system;
modules = [ modules = [
self.nixosModules.machine selfhostblocks.nixosModules.${system}.default
]; ];
};
deploy.nodes.machine = { # Machine specific configuration goes here.
hostname = ...;
sshUser = ...;
sshOpts = [ ... ];
profiles = {
system = {
user = "root";
path = deployPkgs.deploy-rs.lib.activate.nixos self.nixosConfigurations.machine;
}; };
}; };
}; };
# From https://github.com/serokell/deploy-rs?tab=readme-ov-file#overall-usage
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
};
} }
``` ```
The above snippet assumes one machine to deploy to, so `nixpkgs` is defined ## Example Deployment With Colmena {#usage-example-colmena}
exclusively by the `selfhostblocks` input. It is more likely that you have
multiple machines, some not using SHB, in this case you can do: The following snippets show how to deploy Self Host Blocks using the deployment system [Colmena][3].
[3]: https://colmena.cli.rs
```nix ```nix
{ {
@ -459,108 +123,96 @@ multiple machines, some not using SHB, in this case you can do:
}; };
outputs = { self, selfhostblocks }: { outputs = { self, selfhostblocks }: {
colmena =
let let
system = "x86_64-linux"; system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs; in {
pkgs' = import nixpkgs' { meta = {
inherit system; nixpkgs = import selfhostblocks.inputs.nixpkgs { inherit system; };
}; };
deployPkgs = import selfhostblocks.inputs.nixpkgs { machine = { selfhostblocks, ... }: {
inherit system;
overlays = [
deploy-rs.overlay
(self: super: {
deploy-rs = {
inherit (pkgs') deploy-rs;
lib = super.deploy-rs.lib;
};
})
];
};
in
nixosModules.machine1 = {
# ...
};
nixosModules.machine2 = {
imports = [ imports = [
selfhostblocks.nixosModules.default selfhostblocks.nixosModules.${system}.default
]; ];
};
nixosConfigurations.machine1 = nixpkgs.lib.nixosSystem { # Machine specific configuration goes here.
inherit system;
modules = [
self.nixosModules.machine1
];
};
nixosConfigurations.machine2 = nixpkgs'.nixosSystem {
inherit system;
modules = [
self.nixosModules.machine2
];
};
deploy.nodes.machine1 = {
hostname = ...;
sshUser = ...;
sshOpts = [ ... ];
profiles = {
system = {
user = "root";
path = deployPkgs.deploy-rs.lib.activate.nixos self.nixosConfigurations.machine1;
}; };
}; };
}; };
deploy.nodes.machine2 = # Similar here
# From https://github.com/serokell/deploy-rs?tab=readme-ov-file#overall-usage
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
};
} }
``` ```
In the above snippet, `machine1` will use the `nixpkgs` version from your inputs The above snippet is very minimal as it assumes you have only one machine to deploy to, so `nixpkgs`
while `machine2` will use the `nixpkgs` version from `selfhostblocks`. is defined exclusively by the `selfhostblocks` input. It is more likely that you have multiple machines, in this case you can use the `colmena.meta.nodeNixpkgs` option:
```nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
selfhostblocks.url = "github:ibizaman/selfhostblocks";
};
outputs = { self, selfhostblocks }: {
colmena = {
let
system = "x86_64-linux";
in {
meta =
nixpkgs = import nixpkgs { inherit system; };
nodeNixpkgs = {
machine2 = import selfhostblocks.inputs.nixpkgs { inherit system; };
};
};
machine1 = ...;
machine2 = { selfhostblocks, ... }: {
imports = [
selfhostblocks.nixosModules.${system}.default
];
# Machine specific configuration goes here.
};
};
};
}
```
In the above snippet, `machine1` will use the `nixpkgs` version from your inputs while `machine2`
will use the `nixpkgs` version from `selfhostblocks`.
## Secrets with sops-nix {#usage-secrets} ## Secrets with sops-nix {#usage-secrets}
This section complements the official This section complements the official [sops-nix](https://github.com/Mic92/sops-nix) guide.
[sops-nix](https://github.com/Mic92/sops-nix) guide.
Managing secrets is an important aspect of deploying. You cannot store your Managing secrets is an important aspect of deploying. You cannot store your secrets in nix directly
secrets in nix directly because they get stored unencrypted and you don't want because they get stored unencrypted and you don't want that. We need to use another system that
that. We need to use another system that encrypts secrets when storing in the encrypts secrets when storing in the nix store and then decrypts them on the target host upon system
nix store and then decrypts them on the target host upon system activation. activation. `sops-nix` is one of such system.
`sops-nix` is one of such system.
Sops-nix works by encrypting the secrets file with at least 2 keys. Your private Sops-nix works by encrypting the secrets file with at least 2 keys. Your private key and a private
key and a private key from the target host. This way, you can edit the secrets key from the target host. This way, you can edit the secrets and the target host can decrypt the
and the target host can decrypt the secrets. Separating the keys this way is secrets. Separating the keys this way is good practice because it reduces the impact of having one
good practice because it reduces the impact of having one being compromised. being compromised.
One way to setup secrets management using `sops-nix`: One way to setup secrets management using `sops-nix`:
1. Create your own private key that will be located in `keys.txt`. The public 1. Create your own private key that will be located in `keys.txt`. The public key will be printed on stdout.
key will be printed on stdout.
```bash ```bash
$ nix shell nixpkgs#age --command age-keygen -o keys.txt $ nix shell nixpkgs#age --command age-keygen -o keys.txt
Public key: age1algdv9xwjre3tm7969eyremfw2ftx4h8qehmmjzksrv7f2qve9dqg8pug7 Public key: age1algdv9xwjre3tm7969eyremfw2ftx4h8qehmmjzksrv7f2qve9dqg8pug7
``` ```
2. Get the target host's public key. We will use the key derived from the ssh 2. Get the target host's public key. We will use the key derived from the ssh key of the host.
key of the host.
```bash ```bash
$ nix shell nixpkgs#ssh-to-age --command \ $ nix shell nixpkgs#ssh-to-age --command \
sh -c 'ssh-keyscan -t ed25519 -4 <target_host> | ssh-to-age' sh -c 'ssh-keyscan -t ed25519 -4 <target_host> | ssh-to-age'
# localhost:2222 SSH-2.0-OpenSSH_9.6 # localhost:2222 SSH-2.0-OpenSSH_9.6
age13wgyyae8epyw894ugd0rjjljh0rm98aurvzmsapcv7d852g9r5lq0pqfx8 age13wgyyae8epyw894ugd0rjjljh0rm98aurvzmsapcv7d852g9r5lq0pqfx8
``` ```
3. Create a `sops.yaml` file that explains how sops-nix should encrypt the - yet 3. Create a `sops.yaml` file that explains how sops-nix should encrypt the - yet to be created -
to be created - `secrets.yaml` file. You can be creative here, but a basic `secrets.yaml` file. You can be creative here, but a basic snippet is:
snippet is:
```bash ```bash
keys: keys:
- &me age1algdv9xwjre3tm7969eyremfw2ftx4h8qehmmjzksrv7f2qve9dqg8pug7 - &me age1algdv9xwjre3tm7969eyremfw2ftx4h8qehmmjzksrv7f2qve9dqg8pug7
@ -572,14 +224,12 @@ One way to setup secrets management using `sops-nix`:
- *me - *me
- *target - *target
``` ```
4. Create a `secrets.yaml` file that will contain the encrypted secrets as a 4. Create a `secrets.yaml` file that will contain the encrypted secrets as a Yaml file:
Yaml file:
```bash ```bash
$ SOPS_AGE_KEY_FILE=keys.txt nix run --impure nixpkgs#sops -- \ $ SOPS_AGE_KEY_FILE=keys.txt nix run --impure nixpkgs#sops -- \
secrets.yaml secrets.yaml
``` ```
This will open your preferred editor. An example of yaml file is the This will open your preferred editor. An example of yaml file is the following (secrets are elided for brevity):
following (secrets are elided for brevity):
```yaml ```yaml
nextcloud: nextcloud:
adminpass: 43bb4b... adminpass: 43bb4b...
@ -622,20 +272,18 @@ One way to setup secrets management using `sops-nix`:
5. Use `sops-nix` module in nix: 5. Use `sops-nix` module in nix:
```bash ```bash
imports = [ imports = [
inputs.sops-nix.nixosModules.default selfhostblocks.inputs.sops-nix.nixosModules.default
inputs.selfhostblocks.nixosModules.sops
]; ];
``` ```
Import also the `sops` module provided by SHB. 6. Reference the secrets in nix:
6. Set default sops file:
```bash
sops.defaultSopsFile = ./secrets.yaml;
```
Setting the default this way makes all sops instances use that same file.
7. Reference the secrets in nix:
```nix ```nix
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request; shb.nextcloud.adminPassFile = config.sops.secrets."nextcloud/adminpass".path;
shb.nextcloud.adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
sops.secrets."nextcloud/adminpass" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "nextcloud";
group = "nextcloud";
restartUnits = [ "phpfpm-nextcloud.service" ];
};
``` ```
The above snippet uses the [secrets contract](./contracts-secret.html) and
[sops block](./blocks-sops.html) to ease the configuration.

View file

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1710146030,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nix-flake-tests": { "nix-flake-tests": {
"locked": { "locked": {
"lastModified": 1775571237, "lastModified": 1677844186,
"narHash": "sha256-1f5Uvgcy3gx/eyRp4rqfDRBjcZc9uiHoIlfcFIL7GvI=", "narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
"owner": "antifuchs", "owner": "antifuchs",
"repo": "nix-flake-tests", "repo": "nix-flake-tests",
"rev": "86b789cff8aecbd7c1bed7cd421dd837c3f62201", "rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -35,11 +35,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1781074563, "lastModified": 1716769173,
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", "narHash": "sha256-7EXDb5WBw+d004Agt+JHC/Oyh/KTUglOaQ4MNjBbo5w=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", "rev": "9ca3f649614213b2aaf5f1e16ec06952fe4c2632",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -49,14 +49,46 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-stable": {
"locked": {
"lastModified": 1716655032,
"narHash": "sha256-kQ25DAiCGigsNR/Quxm3v+JGXAEXZ8I7RAF4U94bGzE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "59a450646ec8ee0397f5fa54a08573e8240eb91f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1716651315,
"narHash": "sha256-iMgzIeedMqf30TXZ439zW3Yvng1Xm9QTGO+ZwG1IWSw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c5187508b11177ef4278edf19616f44f21cc8c69",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nmdsrc": { "nmdsrc": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1757277932, "lastModified": 1705050560,
"narHash": "sha256-eW2AzE4HeJXHKxSGDcp35/VQyxeESBt4p9i8QIFrSE8=", "narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "055e814ac35379dece164a8413d336b57c5d2e0c", "rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"revCount": 67, "revCount": 66,
"type": "git", "type": "git",
"url": "https://git.sr.ht/~rycee/nmd" "url": "https://git.sr.ht/~rycee/nmd"
}, },
@ -70,7 +102,27 @@
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nix-flake-tests": "nix-flake-tests", "nix-flake-tests": "nix-flake-tests",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"nmdsrc": "nmdsrc" "nmdsrc": "nmdsrc",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1716692524,
"narHash": "sha256-sALodaA7Zkp/JD6ehgwc0UCBrSBfB4cX66uFGTsqeFU=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "962797a8d7f15ed7033031731d0bb77244839960",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
} }
}, },
"systems": { "systems": {

555
flake.nix
View file

@ -3,6 +3,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
sops-nix.url = "github:Mic92/sops-nix";
nix-flake-tests.url = "github:antifuchs/nix-flake-tests"; nix-flake-tests.url = "github:antifuchs/nix-flake-tests";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
nmdsrc = { nmdsrc = {
@ -11,504 +12,130 @@
}; };
}; };
outputs = outputs = { nixpkgs, nix-flake-tests, flake-utils, nmdsrc, ... }: flake-utils.lib.eachDefaultSystem (system:
inputs@{
self,
nixpkgs,
nix-flake-tests,
flake-utils,
nmdsrc,
...
}:
let let
shbPatches = originPkgs = nixpkgs.legacyPackages.${system};
system: patches = [
nixpkgs.legacyPackages.${system}.lib.optionals (originPkgs.fetchpatch {
(system == "x86_64-linux" || system == "aarch64-linux") url = "https://patch-diff.githubusercontent.com/raw/NixOS/nixpkgs/pull/315018.patch";
[ hash = "sha256-8jcGyO/d+htfv/ZajxXh89S3OiDZAr7/fsWC1JpGczM=";
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged. })
./patches/lldap.patch (originPkgs.fetchpatch {
./patches/0001-nixos-borgbackup-add-option-to-override-state-direct.patch url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
hash = "sha256-6SfqnPLPxJHckXNU03HA0X03u9Ynn3baQ2HHMq5FkIc=";
# Leaving commented out as an example. })
# (originPkgs.fetchpatch { (originPkgs.fetchpatch {
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch"; url = "https://patch-diff.githubusercontent.com/raw/NixOS/nixpkgs/pull/304721.patch";
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk="; sha256 = "sha256-AOTtqbsDkykJKeE/e0Y2IscSHZ56vLEbl7lzxd+QfKo=";
# }) })
]; ];
patchedNixpkgs = originPkgs.applyPatches {
patchNixpkgs =
{
nixpkgs,
patches,
system,
}:
nixpkgs.legacyPackages.${system}.applyPatches {
name = "nixpkgs-patched"; name = "nixpkgs-patched";
src = nixpkgs; src = nixpkgs;
inherit patches; inherit patches;
}; };
patchedNixpkgs =
system:
let
patched = patchNixpkgs {
nixpkgs = inputs.nixpkgs;
patches = shbPatches system;
inherit system;
};
in
patched
// {
nixosSystem = args: import "${patched}/nixos/lib/eval-config.nix" args;
};
pkgs' =
system:
import (patchedNixpkgs system) {
inherit system;
config.allowUnfree = true;
};
in
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = pkgs' system;
# The contract dummies are used to show options for contracts. pkgs = import patchedNixpkgs {
inherit system;
};
allModules = [
modules/blocks/authelia.nix
modules/blocks/backup.nix
modules/blocks/davfs.nix
modules/blocks/ldap.nix
modules/blocks/monitoring.nix
modules/blocks/nginx.nix
modules/blocks/postgresql.nix
modules/blocks/ssl.nix
modules/blocks/tinyproxy.nix
modules/blocks/vpn.nix
modules/services/arr.nix
modules/services/audiobookshelf.nix
modules/services/deluge.nix
modules/services/grocy.nix
modules/services/hledger.nix
modules/services/home-assistant.nix
modules/services/jellyfin.nix
modules/services/nextcloud-server.nix
modules/services/vaultwarden.nix
];
# Only used for documentation.
contractDummyModules = [ contractDummyModules = [
modules/contracts/backup/dummyModule.nix
modules/contracts/dashboard/dummyModule.nix
modules/contracts/databasebackup/dummyModule.nix
modules/contracts/datasetbackup/dummyModule.nix
modules/contracts/secret/dummyModule.nix
modules/contracts/ssl/dummyModule.nix modules/contracts/ssl/dummyModule.nix
]; ];
in in
{ {
formatter = pkgs.nixfmt-tree; nixosModules.default = { config, ... }: {
imports = allModules;
};
packages.manualHtml = pkgs.callPackage ./docs { packages.manualHtml = pkgs.callPackage ./docs {
inherit nmdsrc; inherit nmdsrc;
allModules = allModules = allModules ++ contractDummyModules;
self.nixosModules.default.imports release = "0.0.1";
++ [
self.nixosModules.sops
]
++ contractDummyModules;
release = builtins.readFile ./VERSION;
substituteVersionIn = [
"./manual.md"
"./usage.md"
];
modules = {
"blocks/authelia" = ./modules/blocks/authelia.nix;
"blocks/borgbackup" = ./modules/blocks/borgbackup.nix;
"blocks/lldap" = ./modules/blocks/lldap.nix;
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
"blocks/nginx" = ./modules/blocks/nginx.nix;
"blocks/postgresql" = ./modules/blocks/postgresql.nix;
"blocks/restic" = ./modules/blocks/restic.nix;
"blocks/sanoid" = ./modules/blocks/sanoid.nix;
"blocks/sops" = ./modules/blocks/sops.nix;
"blocks/ssl" = {
module = ./modules/blocks/ssl.nix;
optionRoot = [
"shb"
"certs"
];
};
"blocks/zfs" = ./modules/blocks/zfs.nix;
"services/arr" = ./modules/services/arr.nix;
"services/firefly-iii" = ./modules/services/firefly-iii.nix;
"services/forgejo" = [
./modules/services/forgejo.nix
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
];
"services/home-assistant" = ./modules/services/home-assistant.nix;
"services/homepage" = ./modules/services/homepage.nix;
"services/jellyfin" = ./modules/services/jellyfin.nix;
"services/karakeep" = ./modules/services/karakeep.nix;
"services/mailserver" = ./modules/services/mailserver.nix;
"services/nextcloud-server" = {
module = ./modules/services/nextcloud-server.nix;
optionRoot = [
"shb"
"nextcloud"
];
};
"services/open-webui" = ./modules/services/open-webui.nix;
"services/pinchflat" = ./modules/services/pinchflat.nix;
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
"contracts/backup" = {
module = ./modules/contracts/backup/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"backup"
];
};
"contracts/dashboard" = {
module = ./modules/contracts/dashboard/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"dashboard"
];
};
"contracts/databasebackup" = {
module = ./modules/contracts/databasebackup/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"databasebackup"
];
};
"contracts/datasetbackup" = {
module = ./modules/contracts/datasetbackup/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"datasetbackup"
];
};
"contracts/secret" = {
module = ./modules/contracts/secret/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"secret"
];
};
"contracts/ssl" = {
module = ./modules/contracts/ssl/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"ssl"
];
};
};
}; };
# Documentation redirect generation tool - scans HTML files for anchor mappings lib.contracts = pkgs.callPackage ./modules/contracts {};
packages.generateRedirects =
let
# Python patch to inject redirect collector
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
# Load redirect collector patch
try:
import sys, os
sys.path.insert(0, os.path.dirname(__file__) + '/..')
import missing_refs_collector
except Exception as e:
print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr)
'';
# Patched nixos-render-docs that collects redirects during HTML generation
nixos-render-docs-patched = pkgs.writeShellApplication {
name = "nixos-render-docs";
runtimeInputs = [ pkgs.nixos-render-docs ];
text = ''
TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT
cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/"
chmod -R +w "$TEMP_DIR"
cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py"
echo '{}' > "$TEMP_DIR/empty_redirects.json"
cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py"
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;;
*) ARGS+=("$1"); shift ;;
esac
done
export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}"
nixos-render-docs "''${ARGS[@]}"
'';
};
in
(self.packages.${system}.manualHtml.override {
nixos-render-docs = nixos-render-docs-patched;
}).overrideAttrs
(old: {
installPhase = ''
${old.installPhase}
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
'';
});
packages.manualHtml-watch = pkgs.writeShellApplication {
name = "manualHtml-watch";
runtimeInputs = [
pkgs.findutils
pkgs.entr
];
text = ''
while sleep 1; do
find . -name "*.nix" -o -name "*.md" \
| entr -d sh -c '(nix run --offline .#update-redirects && nix build --offline .#manualHtml)' || :
done
'';
};
packages.update-flake-lock-pr = pkgs.callPackage ./.github/workflows/update-flake-lock-pr.nix { };
lib = (pkgs.callPackage ./lib { }) // {
test = pkgs.callPackage ./test/common.nix { };
contracts = pkgs.callPackage ./modules/contracts {
shb = self.lib.${system};
};
patches = shbPatches system;
inherit patchNixpkgs;
patchedNixpkgs = patchedNixpkgs system;
};
# To see the traces, run:
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
packages.playwright = pkgs.callPackage (
{
stdenvNoCC,
makeWrapper,
playwright,
}:
stdenvNoCC.mkDerivation {
name = "playwright";
src = playwright;
nativeBuildInputs = [
makeWrapper
];
# No quotes around the value for LLDAP_PASSWORD because we want the value to not be enclosed in quotes.
installPhase = ''
makeWrapper ${pkgs.python3Packages.playwright}/bin/playwright $out/bin/playwright \
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
'';
}
) { };
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
apps.update-redirects = {
type = "app";
program = "${
pkgs.writeShellApplication {
name = "update-redirects";
runtimeInputs = [
pkgs.nix
pkgs.jq
];
text = ''
echo "=== SelfHostBlocks Redirects Updater ==="
echo "Generating fresh ./docs/redirects.json..."
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
cp result/redirects.json docs/redirects.json
echo " Updated docs/redirects.json"
echo "To verify: nix build .#manualHtml"
'';
}
}/bin/update-redirects";
};
}
)
// flake-utils.lib.eachSystem [ "x86_64-linux" ] (
system:
let
pkgs = pkgs' system;
in
{
checks = checks =
let let
inherit (pkgs.lib) importFiles = files:
foldl map (m: pkgs.callPackage m {}) files;
foldlAttrs
mergeAttrs
;
importFiles = mergeTests = pkgs.lib.lists.foldl pkgs.lib.trivial.mergeAttrs {};
files:
map (
m:
pkgs.callPackage m {
shb = self.lib.${system};
}
) files;
mergeTests = foldl mergeAttrs { }; flattenAttrs = root: attrset: pkgs.lib.attrsets.foldlAttrs (acc: name: value: acc // {
flattenAttrs =
root: attrset:
foldlAttrs (
acc: name: value:
acc
// {
"${root}_${name}" = value; "${root}_${name}" = value;
} }) {} attrset;
) { } attrset;
vm_test = vm_test = name: path: flattenAttrs "vm_${name}" (
name: path: import path {
flattenAttrs "vm_${name}" (
removeAttrs
(pkgs.callPackage path {
shb = self.lib.${system};
})
[
"override"
"overrideDerivation"
]
);
in
(
{
modules = self.lib.${system}.check {
inherit pkgs; inherit pkgs;
tests = mergeTests (importFiles [ inherit (pkgs) lib;
}
);
shblib = pkgs.callPackage ./lib {};
in (rec {
all = mergeTests [
modules
];
modules = shblib.check {
inherit pkgs;
tests =
mergeTests (importFiles [
./test/modules/arr.nix
./test/modules/davfs.nix ./test/modules/davfs.nix
# TODO: Make this not use IFD
./test/modules/lib.nix ./test/modules/lib.nix
./test/modules/nginx.nix
./test/modules/postgresql.nix
]); ]);
}; };
# TODO: Make this not use IFD
lib = nix-flake-tests.lib.check { lib = nix-flake-tests.lib.check {
inherit pkgs; inherit pkgs;
tests = pkgs.callPackage ./test/modules/lib.nix { tests = pkgs.callPackage ./test/modules/lib.nix {};
shb = self.lib.${system};
};
}; };
} }
// (vm_test "arr" ./test/services/arr.nix) // (vm_test "arr" ./test/vm/arr.nix)
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) // (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix)
// (vm_test "deluge" ./test/services/deluge.nix) // (vm_test "authelia" ./test/vm/authelia.nix)
// (vm_test "firefly-iii" ./test/services/firefly-iii.nix) // (vm_test "deluge" ./test/vm/deluge.nix)
// (vm_test "forgejo" ./test/services/forgejo.nix) // (vm_test "grocy" ./test/vm/grocy.nix)
// (vm_test "grocy" ./test/services/grocy.nix) // (vm_test "home-assistant" ./test/vm/home-assistant.nix)
// (vm_test "hledger" ./test/services/hledger.nix) // (vm_test "jellyfin" ./test/vm/jellyfin.nix)
// (vm_test "immich" ./test/services/immich.nix) // (vm_test "ldap" ./test/vm/ldap.nix)
// (vm_test "homeassistant" ./test/services/home-assistant.nix) // (vm_test "lib" ./test/vm/lib.nix)
// (vm_test "homepage" ./test/services/homepage.nix) // (vm_test "monitoring" ./test/vm/monitoring.nix)
// (vm_test "jellyfin" ./test/services/jellyfin.nix) // (vm_test "nextcloud" ./test/vm/nextcloud.nix)
// (vm_test "karakeep" ./test/services/karakeep.nix) // (vm_test "postgresql" ./test/vm/postgresql.nix)
// (vm_test "mailserver" ./test/services/mailserver.nix) // (vm_test "ssl" ./test/vm/ssl.nix)
// (vm_test "nextcloud" ./test/services/nextcloud.nix) // (vm_test "vaultwarden" ./test/vm/vaultwarden.nix)
// (vm_test "open-webui" ./test/services/open-webui.nix)
// (vm_test "paperless" ./test/services/paperless.nix)
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
// (vm_test "authelia" ./test/blocks/authelia.nix)
// (vm_test "borgbackup" ./test/blocks/borgbackup.nix)
// (vm_test "lib" ./test/blocks/lib.nix)
// (vm_test "lldap" ./test/blocks/lldap.nix)
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
// (vm_test "monitoring" ./test/blocks/monitoring.nix)
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
// (vm_test "restic" ./test/blocks/restic.nix)
// (vm_test "ssl" ./test/blocks/ssl.nix)
// (vm_test "zfs" ./test/blocks/zfs.nix)
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
// (vm_test "contracts-datasetbackup" ./test/contracts/datasetbackup.nix)
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
); );
} }
) );
// {
herculesCI.ciSystems = [ "x86_64-linux" ];
nixosModules.default = {
imports = [
# blocks
self.nixosModules.authelia
self.nixosModules.borgbackup
self.nixosModules.davfs
self.nixosModules.hardcodedsecret
self.nixosModules.lldap
self.nixosModules.mitmdump
self.nixosModules.monitoring
self.nixosModules.nginx
self.nixosModules.postgresql
self.nixosModules.restic
self.nixosModules.sanoid
self.nixosModules.ssl
self.nixosModules.tinyproxy
self.nixosModules.vpn
self.nixosModules.zfs
# services
self.nixosModules.arr
self.nixosModules.audiobookshelf
self.nixosModules.deluge
self.nixosModules.firefly-iii
self.nixosModules.forgejo
self.nixosModules.grocy
self.nixosModules.hledger
self.nixosModules.immich
self.nixosModules.home-assistant
self.nixosModules.homepage
self.nixosModules.jellyfin
self.nixosModules.karakeep
self.nixosModules.mailserver
self.nixosModules.nextcloud-server
self.nixosModules.open-webui
self.nixosModules.pinchflat
self.nixosModules.paperless
self.nixosModules.vaultwarden
];
};
nixosModules.lib = lib/module.nix;
nixosModules.authelia = modules/blocks/authelia.nix;
nixosModules.borgbackup = modules/blocks/borgbackup.nix;
nixosModules.davfs = modules/blocks/davfs.nix;
nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix;
nixosModules.lldap = modules/blocks/lldap.nix;
nixosModules.mitmdump = modules/blocks/mitmdump.nix;
nixosModules.monitoring = modules/blocks/monitoring.nix;
nixosModules.nginx = modules/blocks/nginx.nix;
nixosModules.postgresql = modules/blocks/postgresql.nix;
nixosModules.restic = modules/blocks/restic.nix;
nixosModules.sanoid = modules/blocks/sanoid.nix;
nixosModules.sops = modules/blocks/sops.nix;
nixosModules.ssl = modules/blocks/ssl.nix;
nixosModules.tinyproxy = modules/blocks/tinyproxy.nix;
nixosModules.vpn = modules/blocks/vpn.nix;
nixosModules.zfs = modules/blocks/zfs.nix;
nixosModules.arr = modules/services/arr.nix;
nixosModules.audiobookshelf = modules/services/audiobookshelf.nix;
nixosModules.deluge = modules/services/deluge.nix;
nixosModules.firefly-iii = modules/services/firefly-iii.nix;
nixosModules.forgejo = modules/services/forgejo.nix;
nixosModules.grocy = modules/services/grocy.nix;
nixosModules.hledger = modules/services/hledger.nix;
nixosModules.immich = modules/services/immich.nix;
nixosModules.home-assistant = modules/services/home-assistant.nix;
nixosModules.homepage = modules/services/homepage.nix;
nixosModules.jellyfin = modules/services/jellyfin.nix;
nixosModules.karakeep = modules/services/karakeep.nix;
nixosModules.mailserver = modules/services/mailserver.nix;
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
nixosModules.open-webui = modules/services/open-webui.nix;
nixosModules.paperless = modules/services/paperless.nix;
nixosModules.pinchflat = modules/services/pinchflat.nix;
nixosModules.vaultwarden = modules/services/vaultwarden.nix;
};
} }

View file

@ -1,26 +1,11 @@
{ pkgs, lib }: { pkgs, lib }:
let rec {
inherit (builtins) isAttrs hasAttr;
inherit (lib)
any
concatMapStringsSep
concatStringsSep
escapeShellArg
;
shb = rec {
# Replace secrets in a file. # Replace secrets in a file.
# - userConfig is an attrset that will produce a config file. # - userConfig is an attrset that will produce a config file.
# - resultPath is the location the config file should have on the filesystem. # - resultPath is the location the config file should have on the filesystem.
# - generator is a function taking two arguments name and value and returning path in the nix # - generator is a function taking two arguments name and value and returning path in the nix
# nix store where the # nix store where the
replaceSecrets = replaceSecrets = { userConfig, resultPath, generator }:
{
userConfig,
resultPath,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let let
configWithTemplates = withReplacements userConfig; configWithTemplates = withReplacements userConfig;
@ -31,108 +16,31 @@ let
replaceSecretsScript { replaceSecretsScript {
file = nonSecretConfigFile; file = nonSecretConfigFile;
inherit resultPath replacements; inherit resultPath replacements;
inherit user permissions;
}; };
replaceSecretsFormatAdapter = format: format.generate; replaceSecretsFormatAdapter = format: format.generate;
replaceSecretsGeneratorAdapter = replaceSecretsGeneratorAdapter = generator: name: value: pkgs.writeText "generator " (generator value);
generator: name: value:
pkgs.writeText "generator " (generator value);
toEnvVar = replaceSecretsGeneratorAdapter (
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
);
template = template = file: newPath: replacements: replaceSecretsScript {
file: newPath: replacements:
replaceSecretsScript {
inherit file replacements; inherit file replacements;
resultPath = newPath; resultPath = newPath;
}; };
genReplacement = replaceSecretsScript = { file, resultPath, replacements }:
secret:
let
t =
{
transform ? null,
...
}:
if isNull transform then x: x else transform;
in
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
replaceSecretsScript =
{
file,
resultPath,
replacements,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let let
templatePath = resultPath + ".template"; templatePath = resultPath + ".template";
sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
# We check that the files containing the secrets have the sedCmd = if replacements == {}
# correct permissions for us to read them in this separate then "cat"
# step. Otherwise, the $(cat ...) commands inside the sed else "${pkgs.gnused}/bin/sed ${sedPatterns}";
# replacements could fail but not fail individually but
# not fail the whole script.
checkPermissions = concatMapStringsSep "\n" (
pattern: "cat ${pattern.source} > /dev/null"
) replacements;
generatedReplacements = map genReplacement replacements;
replaceScript = pkgs.writers.writePython3 "replace-secret" { } ''
import argparse
import pathlib
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--file", required=True, type=pathlib.Path)
parser.add_argument("--marker", required=True)
parser.add_argument("--replacement", required=True)
return parser.parse_args()
args = parse_args()
content = args.file.read_text()
args.file.write_text(content.replace(args.marker, args.replacement))
'';
replaceCommands = concatMapStringsSep "\n" (pattern: ''
${replaceScript} \
--file ${escapeShellArg resultPath} \
--marker ${escapeShellArg pattern.name} \
--replacement "${pattern.value}"
'') generatedReplacements;
replaceCmd =
if replacements == [ ] then
"cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}"
else
''
cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}
${replaceCommands}
'';
in in
'' ''
set -euo pipefail set -euo pipefail
${checkPermissions}
mkdir -p $(dirname ${templatePath}) mkdir -p $(dirname ${templatePath})
ln -fs ${file} ${templatePath} ln -fs ${file} ${templatePath}
rm -f ${resultPath} rm -f ${resultPath}
touch ${resultPath} ${sedCmd} ${templatePath} > ${resultPath}
''
+ (lib.optionalString (user != null) ''
chown ${user} ${resultPath}
'')
+ ''
${replaceCmd}
chmod ${permissions} ${resultPath}
''; '';
secretFileType = lib.types.submodule { secretFileType = lib.types.submodule {
@ -153,27 +61,35 @@ let
}; };
}; };
secretName = secretName = name:
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) name)}%";
withReplacements = withReplacements = attrs:
attrs:
let let
valueOrReplacement = valueOrReplacement = name: value:
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; if !(builtins.isAttrs value && value ? "source")
then value
else secretName name;
in in
mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs; mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs;
getReplacements = getReplacements = attrs:
attrs:
let let
addNameField = addNameField = name: value:
name: value: if !(builtins.isAttrs value && value ? "source")
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; then value
else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs; secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
allSecrets = collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
t = { transform ? null, ... }: if isNull transform then x: x else transform;
genReplacement = secret:
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
in in
collect (v: builtins.isAttrs v && v ? "source") secretsWithName; lib.attrsets.listToAttrs (map genReplacement allSecrets);
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
mapAttrsRecursiveCond = mapAttrsRecursiveCond =
@ -184,16 +100,13 @@ let
# Attribute set or list to recursively map over. # Attribute set or list to recursively map over.
set: set:
let let
recurse = recurse = path: val:
path: val: if builtins.isAttrs val && cond val
if builtins.isAttrs val && cond val then then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val else if builtins.isList val && cond val
else if builtins.isList val && cond val then then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val else f path val;
else in recurse [] set;
f path val;
in
recurse [ ] set;
# Like lib.attrsets.collect but also recurses on lists. # Like lib.attrsets.collect but also recurses on lists.
collect = collect =
@ -210,21 +123,12 @@ let
else else
[]; [];
indent =
i: str:
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
# Generator for XML # Generator for XML
formatXML = formatXML = {
{ enclosingRoot ? null
enclosingRoot ? null, }: {
}: type = with lib.types; let
{ valueType = nullOr (oneOf [
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool bool
int int
float float
@ -232,31 +136,21 @@ let
path path
(attrsOf valueType) (attrsOf valueType)
(listOf valueType) (listOf valueType)
]) ]) // {
// {
description = "XML value"; description = "XML value";
}; };
in in valueType;
valueType;
generate = generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
name: value: value = builtins.toJSON (
pkgs.callPackage ( if enclosingRoot == null then
{ runCommand, python3 }: value
runCommand "config" else
{ { ${enclosingRoot} = value; });
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
passAsFile = [ "value" ]; passAsFile = [ "value" ];
} } (pkgs.writers.writePython3 "dict2xml" {
( libraries = with python3.pkgs; [ python dict2xml ];
pkgs.writers.writePython3 "dict2xml" } ''
{
libraries = with python3.pkgs; [
python
dict2xml
];
}
''
import os import os
import json import json
from dict2xml import dict2xml from dict2xml import dict2xml
@ -268,28 +162,18 @@ let
os.exit(2) os.exit(2)
with open(os.environ["out"], "w") as out: with open(os.environ["out"], "w") as out:
out.write(dict2xml(content)) out.write(dict2xml(content))
'' '')) {};
)
) { };
}; };
parseXML = parseXML = xml:
xml:
let let
xmlToJsonFile = pkgs.callPackage ( xmlToJsonFile = pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
{ runCommand, python3 }:
runCommand "config"
{
inherit xml; inherit xml;
passAsFile = [ "xml" ]; passAsFile = [ "xml" ];
} } (pkgs.writers.writePython3 "xml2json" {
(
pkgs.writers.writePython3 "xml2json"
{
libraries = with python3.pkgs; [ python ]; libraries = with python3.pkgs; [ python ];
} } ''
''
import os import os
import json import json
from collections import ChainMap from collections import ChainMap
@ -312,149 +196,40 @@ let
with open(os.environ["out"], "w") as out: with open(os.environ["out"], "w") as out:
out.write(j) out.write(j)
'' '')) {};
)
) { };
in in
builtins.fromJSON (builtins.readFile xmlToJsonFile); builtins.fromJSON (builtins.readFile xmlToJsonFile);
renameAttrName = renameAttrName = attrset: from: to:
attrset: from: to: (lib.attrsets.filterAttrs (name: v: name == from) attrset) // {
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
// {
${to} = attrset.${from}; ${to} = attrset.${from};
}; };
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
# with a nicer diff display function. # with a nicer diff display function.
check = check = { pkgs, tests }:
{ pkgs, tests }:
let let
formatValue = formatValue = val:
val: if (builtins.isList val || builtins.isAttrs val) then builtins.toJSON val
if (builtins.isList val || builtins.isAttrs val) then else builtins.toString val;
builtins.toJSON val
else
builtins.toString val;
resultToString = resultToString = { name, expected, result }:
{ builtins.readFile (pkgs.runCommand "nix-flake-tests-error" {
name,
expected,
result,
}:
builtins.readFile (
pkgs.runCommand "nix-flake-tests-error"
{
expected = formatValue expected; expected = formatValue expected;
result = formatValue result; result = formatValue result;
passAsFile = [ passAsFile = [ "expected" "result" ];
"expected" } ''
"result"
];
nativeBuildInputs = [
(pkgs.python3.withPackages (ps: [ ps.deepdiff ] ++ ps.deepdiff.optional-dependencies.cli))
];
}
''
echo "${name} failed (- expected, + result)" > $out echo "${name} failed (- expected, + result)" > $out
cp ''${expectedPath} ''${expectedPath}.json cp ''${expectedPath} ''${expectedPath}.json
cp ''${resultPath} ''${resultPath}.json cp ''${resultPath} ''${resultPath}.json
deep diff ''${expectedPath}.json ''${resultPath}.json >> $out ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
'' '');
);
results = pkgs.lib.runTests tests; results = pkgs.lib.runTests tests;
in in
if results != [ ] then if results != [ ] then
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results))) builtins.throw (builtins.concatStringsSep "\n" (map resultToString results))
else else
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
genConfigOutOfBandSystemd =
{
config,
configLocation,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
{
loadCredentials = getLoadCredentials "source" config;
preStart = lib.mkBefore (replaceSecrets {
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
resultPath = configLocation;
inherit generator;
inherit user permissions;
});
};
updateToLoadCredentials =
sourceField: rootDir: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
valueOrLoadCredential =
path: value:
if !(hasPlaceholderField value) then
value
else
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
in
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
getLoadCredentials =
sourceField: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
addPathField =
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
in
map genLoadCredentials allSecrets;
anyNotNull = any (x: x != null);
mkJellyfinPlugin =
{
pname,
version,
hash,
url,
}:
pkgs.callPackage (
{ stdenv, fetchzip }:
stdenv.mkDerivation (finalAttrs: {
inherit pname version;
src = fetchzip {
inherit url hash;
stripRoot = false;
};
dontBuild = true;
installPhase = ''
mkdir $out
cp -r . $out
'';
})
) { };
update =
attr: fn: attrset:
attrset // { ${attr} = fn attrset.${attr}; };
};
in
shb
// {
homepage = pkgs.callPackage ./homepage.nix { inherit shb; };
} }

View file

@ -1,92 +0,0 @@
{ lib, shb }:
let
sort =
attr: vs:
map (v: { ${v.name} = v.${attr}; }) (
lib.sortOn (v: v.sortOrder) (lib.mapAttrsToList (n: v: v // { name = n; }) vs)
);
slufigy = builtins.replaceStrings [ "-" ] [ "_" ];
mkService =
groupName: serviceName:
{
request,
...
}:
apiKey: settings:
lib.recursiveUpdate (
{
href = request.externalUrl;
siteMonitor = if (request.internalUrl == null) then null else request.internalUrl;
icon = "sh-${lib.toLower serviceName}";
}
// lib.optionalAttrs (apiKey != null) {
widget = {
# Duplicating because widgets call the api key various names
# and duplicating is a hacky but easy solution.
key = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
password = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
type = lib.toLower serviceName;
url = if (request.internalUrl != null) then request.internalUrl else request.externalUrl;
};
}
) settings;
asServiceGroup =
cfg:
sort "services" (
lib.mapAttrs (
groupName: groupCfg:
shb.update "services" (
services:
sort "dashboard" (
lib.mapAttrs (
serviceName: serviceCfg:
shb.update "dashboard" (
dashboard:
(mkService groupName serviceName) dashboard serviceCfg.apiKey (serviceCfg.settings or { })
) serviceCfg
) services
)
) groupCfg
) cfg
);
allKeys =
cfg:
let
flat = lib.flatten (
lib.mapAttrsToList (
groupName: groupCfg:
lib.mapAttrsToList (
serviceName: serviceCfg:
lib.optionalAttrs (serviceCfg.apiKey != null) {
inherit serviceName groupName;
inherit (serviceCfg.apiKey.result) path;
}
) groupCfg.services
) cfg
);
flatWithApiKey = builtins.filter (v: v != { }) flat;
in
builtins.listToAttrs (
map (
{
groupName,
serviceName,
path,
}:
lib.nameValuePair "${slufigy groupName}_${slufigy serviceName}" path
) flatWithApiKey
);
in
{
inherit
allKeys
asServiceGroup
mkService
sort
;
}

View file

@ -1,10 +0,0 @@
{ pkgs, lib, ... }:
let
shb = (import ./default.nix { inherit pkgs lib; });
in
{
_module.args.shb = shb // {
test = pkgs.callPackage ../test/common.nix { };
contracts = pkgs.callPackage ../modules/contracts { inherit shb; };
};
}

View file

@ -1,33 +1,17 @@
{ { config, pkgs, lib, ... }:
config,
options,
pkgs,
lib,
shb,
...
}:
let let
cfg = config.shb.authelia; cfg = config.shb.authelia;
opt = options.shb.authelia;
contracts = pkgs.callPackage ../contracts {};
shblib = pkgs.callPackage ../../lib {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}"; fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
autheliaCfg = config.services.authelia.instances.${fqdn}; autheliaCfg = config.services.authelia.instances.${fqdn};
inherit (lib) hasPrefix;
listenPort = if cfg.debug then 9090 else 9091;
in in
{ {
imports = [
../../lib/module.nix
./lldap.nix
./mitmdump.nix
./postgresql.nix
];
options.shb.authelia = { options.shb.authelia = {
enable = lib.mkEnableOption "selfhostblocks.authelia"; enable = lib.mkEnableOption "selfhostblocks.authelia";
@ -51,22 +35,16 @@ in
ssl = lib.mkOption { ssl = lib.mkOption {
description = "Path to SSL files"; description = "Path to SSL files";
type = lib.types.nullOr shb.contracts.ssl.certs; type = lib.types.nullOr contracts.ssl.certs;
default = null; default = null;
}; };
ldapHostname = lib.mkOption { ldapEndpoint = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Hostname of the LDAP authentication backend."; description = "Endpoint for LDAP authentication backend.";
example = "ldap.example.com"; example = "ldap.example.com";
}; };
ldapPort = lib.mkOption {
type = lib.types.port;
description = "Port of the LDAP authentication backend.";
example = "389";
};
dcdomain = lib.mkOption { dcdomain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "dc domain for ldap."; description = "dc domain for ldap.";
@ -83,142 +61,68 @@ in
description = "Secrets needed by Authelia"; description = "Secrets needed by Authelia";
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
jwtSecret = lib.mkOption { jwtSecretFile = lib.mkOption {
description = "JWT secret."; type = lib.types.path;
type = lib.types.submodule { description = "File containing the JWT secret.";
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
}; };
ldapAdminPasswordFile = lib.mkOption {
type = lib.types.path;
description = "File containing the LDAP admin user password.";
}; };
sessionSecretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the session secret.";
}; };
ldapAdminPassword = lib.mkOption { storageEncryptionKeyFile = lib.mkOption {
description = "LDAP admin user password."; type = lib.types.path;
type = lib.types.submodule { description = "File containing the storage encryption key.";
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
}; };
identityProvidersOIDCHMACSecretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the identity provider OIDC HMAC secret.";
}; };
}; identityProvidersOIDCIssuerPrivateKeyFile = lib.mkOption {
sessionSecret = lib.mkOption { type = lib.types.path;
description = "Session secret.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
};
};
};
storageEncryptionKey = lib.mkOption {
description = "Storage encryption key. Must be >= 20 characters.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
};
};
};
identityProvidersOIDCHMACSecret = lib.mkOption {
description = "Identity provider OIDC HMAC secret. Must be >= 40 characters.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
};
};
};
identityProvidersOIDCIssuerPrivateKey = lib.mkOption {
description = '' description = ''
Identity provider OIDC issuer private key. File containing the identity provider OIDC issuer private key.
Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048` Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048`
''; '';
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
}; };
}; };
}; };
}; };
};
};
extraOidcClaimsPolicies = lib.mkOption {
description = "Extra OIDC claims policies.";
type = lib.types.attrsOf lib.types.attrs;
default = { };
};
extraOidcScopes = lib.mkOption {
description = "Extra OIDC scopes.";
type = lib.types.attrsOf lib.types.attrs;
default = { };
};
extraOidcAuthorizationPolicies = lib.mkOption {
description = "Extra OIDC authorization policies.";
type = lib.types.attrsOf lib.types.attrs;
default = { };
};
extraDefinitions = lib.mkOption {
description = "Extra definitions.";
type = lib.types.attrsOf lib.types.attrs;
default = { };
};
oidcClients = lib.mkOption { oidcClients = lib.mkOption {
description = "OIDC clients"; description = "OIDC clients";
default = [ default = [
{ {
client_id = "dummy_client"; id = "dummy_client";
client_name = "Dummy Client so Authelia can start"; description = "Dummy Client so Authelia can start";
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret"; secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
public = false; public = false;
authorization_policy = "one_factor"; authorization_policy = "one_factor";
redirect_uris = []; redirect_uris = [];
} }
]; ];
type = lib.types.listOf ( type = lib.types.listOf (lib.types.submodule {
lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything; freeformType = lib.types.attrsOf lib.types.anything;
options = { options = {
client_id = lib.mkOption { id = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Unique identifier of the OIDC client."; description = "Unique identifier of the OIDC client.";
}; };
client_name = lib.mkOption { description = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
description = "Human readable description of the OIDC client."; description = "Human readable description of the OIDC client.";
default = null; default = null;
}; };
client_secret = lib.mkOption { secret = lib.mkOption {
type = shb.secretFileType; type = shblib.secretFileType;
description = '' description = "File containing the shared secret with the OIDC client.";
File containing the shared secret with the OIDC client.
Generate with:
```
nix run nixpkgs#authelia -- \
crypto hash generate pbkdf2 \
--variant sha512 \
--random \
--random.length 72 \
--random.charset rfc3986
```
'';
}; };
public = lib.mkOption { public = lib.mkOption {
@ -229,13 +133,7 @@ in
}; };
authorization_policy = lib.mkOption { authorization_policy = lib.mkOption {
type = lib.types.enum ( type = lib.types.enum [ "one_factor" "two_factor" ];
[
"one_factor"
"two_factor"
]
++ lib.attrNames cfg.extraOidcAuthorizationPolicies
);
description = "Require one factor (password) or two factor (device) authentication."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -247,29 +145,12 @@ in
scopes = lib.mkOption { scopes = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims"; description = "Scopes to ask for";
example = [ example = [ "openid" "profile" "email" "groups" ];
"openid"
"profile"
"email"
"groups"
];
default = []; default = [];
}; };
claims_policy = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Claim policy.
Defaults to 'default' to provide a backwards compatible experience.
Read [this document](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) for more information.
'';
default = "default";
}; };
}; });
}
);
}; };
smtp = lib.mkOption { smtp = lib.mkOption {
@ -282,8 +163,7 @@ in
default = "/tmp/authelia-notifications"; default = "/tmp/authelia-notifications";
type = lib.types.oneOf [ type = lib.types.oneOf [
lib.types.str lib.types.str
(lib.types.nullOr ( (lib.types.nullOr (lib.types.submodule {
lib.types.submodule {
options = { options = {
from_address = lib.mkOption { from_address = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -295,15 +175,6 @@ in
description = "SMTP name from which the emails originate."; description = "SMTP name from which the emails originate.";
default = "Authelia"; default = "Authelia";
}; };
scheme = lib.mkOption {
description = "The protocl must be smtp, submission, or submissions. The only difference between these schemes are the default ports and submissions requires a TLS transport per SMTP Ports Security Measures, whereas submission and smtp use a standard TCP transport and typically enforce StartTLS.";
type = lib.types.enum [
"smtp"
"submission"
"submissions"
];
default = "smtp";
};
host = lib.mkOption { host = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "SMTP host to send the emails to."; description = "SMTP host to send the emails to.";
@ -317,19 +188,12 @@ in
type = lib.types.str; type = lib.types.str;
description = "Username to connect to the SMTP host."; description = "Username to connect to the SMTP host.";
}; };
password = lib.mkOption { passwordFile = lib.mkOption {
type = lib.types.str;
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}.service" ];
}; };
}; };
}; }))
};
}
))
]; ];
}; };
@ -338,72 +202,6 @@ in
description = "Rule based clients"; description = "Rule based clients";
default = []; default = [];
}; };
mount = lib.mkOption {
type = shb.contracts.mount;
description = ''
Mount configuration. This is an output option.
Use it to initialize a block implementing the "mount" contract.
For example, with a zfs dataset:
```
shb.zfs.datasets."authelia" = {
poolName = "root";
} // config.shb.authelia.mount;
```
'';
readOnly = true;
default = {
path = "/var/lib/authelia-authelia.${cfg.domain}";
};
defaultText = {
path = "/var/lib/authelia-authelia.example.com";
};
};
mountRedis = lib.mkOption {
type = shb.contracts.mount;
description = ''
Mount configuration for Redis. This is an output option.
Use it to initialize a block implementing the "mount" contract.
For example, with a zfs dataset:
```
shb.zfs.datasets."redis-authelia" = {
poolName = "root";
} // config.shb.authelia.mountRedis;
```
'';
readOnly = true;
default = {
path = "/var/lib/redis-authelia";
};
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Set logging level to debug and add a mitmdump instance
to see exactly what Authelia receives and sends back.
'';
};
dashboard = lib.mkOption {
description = ''
Dashboard contract consumer
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.dashboard.mkRequester {
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
externalUrlText = "https://\${config.shb.authelia.subdomain}.\${config.shb.authelia.domain}";
internalUrl = "http://127.0.0.1:${toString listenPort}";
};
};
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@ -412,10 +210,6 @@ in
assertion = builtins.length cfg.oidcClients > 0; assertion = builtins.length cfg.oidcClients > 0;
message = "Must have at least one oidc client otherwise Authelia refuses to start."; message = "Must have at least one oidc client otherwise Authelia refuses to start.";
} }
{
assertion = !(hasPrefix "ldap://" cfg.ldapHostname);
message = "LDAP hostname should be the bare host name and not start with ldap://";
}
]; ];
# Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'. # Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'.
@ -432,38 +226,45 @@ in
user = cfg.autheliaUser; user = cfg.autheliaUser;
secrets = { secrets = {
jwtSecretFile = cfg.secrets.jwtSecret.result.path; inherit (cfg.secrets) jwtSecretFile storageEncryptionKeyFile;
storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
sessionSecretFile = cfg.secrets.sessionSecret.result.path;
oidcIssuerPrivateKeyFile = cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
oidcHmacSecretFile = cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
}; };
# See https://www.authelia.com/configuration/methods/secrets/ # See https://www.authelia.com/configuration/methods/secrets/
environmentVariables = { environmentVariables = {
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path; AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPasswordFile;
AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecretFile;
# Not needed since we use peer auth. # Not needed since we use peer auth.
# AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password"; # AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) ( AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKeyFile;
toString cfg.smtp.password.result.path AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecretFile;
); AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKeyFile;
X_AUTHELIA_CONFIG_FILTERS = "template";
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.passwordFile);
}; };
settings = { settings = {
server.address = "tcp://127.0.0.1:${toString listenPort}"; server.host = "127.0.0.1";
server.port = 9091;
# Inspired from https://github.com/lldap/lldap/blob/7d1f5abc137821c500de99c94f7579761fc949d8/example_configs/authelia_config.yml # Inspired from https://github.com/lldap/lldap/blob/7d1f5abc137821c500de99c94f7579761fc949d8/example_configs/authelia_config.yml
authentication_backend = { authentication_backend = {
refresh_interval = "5m"; refresh_interval = "5m";
# We allow password reset and change because the ldap user we use allows it. password_reset = {
password_reset.disable = "false"; disable = "false";
password_change.disable = "false"; };
ldap = { ldap = {
implementation = "lldap"; implementation = "custom";
address = "ldap://${cfg.ldapHostname}:${toString cfg.ldapPort}"; url = cfg.ldapEndpoint;
timeout = "5s"; timeout = "5s";
start_tls = "false"; start_tls = "false";
base_dn = cfg.dcdomain; base_dn = cfg.dcdomain;
# TODO: use user with less privilege and with lldap_password_manager group to be able to change passwords. username_attribute = "uid";
additional_users_dn = "ou=people";
# Sign in with username or email.
users_filter = "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))";
additional_groups_dn = "ou=groups";
groups_filter = "(member={dn})";
group_name_attribute = "cn";
mail_attribute = "mail";
display_name_attribute = "displayName";
user = "uid=admin,ou=people,${cfg.dcdomain}"; user = "uid=admin,ou=people,${cfg.dcdomain}";
}; };
}; };
@ -479,16 +280,11 @@ in
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis # Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
session = { session = {
name = "authelia_session"; name = "authelia_session";
cookies = [
{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}"; domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}
];
same_site = "lax"; same_site = "lax";
expiration = "1h"; expiration = "1h";
inactivity = "5m"; inactivity = "5m";
remember_me = "1M"; remember_me_duration = "1M";
redis = { redis = {
host = config.services.redis.servers.authelia.unixSocket; host = config.services.redis.servers.authelia.unixSocket;
port = 0; port = 0;
@ -496,9 +292,10 @@ in
}; };
storage = { storage = {
postgres = { postgres = {
address = "unix:///run/postgresql"; host = "/run/postgresql";
username = autheliaCfg.user; username = autheliaCfg.user;
database = autheliaCfg.user; database = autheliaCfg.user;
port = config.services.postgresql.port;
# Uses peer auth for local users, so we don't need a password. # Uses peer auth for local users, so we don't need a password.
password = "test"; password = "test";
}; };
@ -508,7 +305,8 @@ in
filename = cfg.smtp; filename = cfg.smtp;
}; };
smtp = lib.mkIf (!(builtins.isString cfg.smtp)) { smtp = lib.mkIf (!(builtins.isString cfg.smtp)) {
address = "${cfg.smtp.scheme}://${cfg.smtp.host}:${toString cfg.smtp.port}"; host = cfg.smtp.host;
port = cfg.smtp.port;
username = cfg.smtp.username; username = cfg.smtp.username;
sender = "${cfg.smtp.from_name} <${cfg.smtp.from_address}>"; sender = "${cfg.smtp.from_name} <${cfg.smtp.from_address}>";
subject = "[Authelia] {title}"; subject = "[Authelia] {title}";
@ -520,11 +318,7 @@ in
networks = [ networks = [
{ {
name = "internal"; name = "internal";
networks = [ networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ];
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/18"
];
} }
]; ];
rules = [ rules = [
@ -535,8 +329,7 @@ in
"^/api/.*" "^/api/.*"
]; ];
} }
] ] ++ cfg.rules;
++ cfg.rules;
}; };
telemetry = { telemetry = {
metrics = { metrics = {
@ -544,28 +337,6 @@ in
address = "tcp://127.0.0.1:9959"; address = "tcp://127.0.0.1:9959";
}; };
}; };
log.level = if cfg.debug then "debug" else "info";
}
// {
identity_providers.oidc = {
claims_policies = {
# This default claim should go away at some point.
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
default.id_token = [
"email"
"preferred_username"
"name"
"groups"
];
}
// cfg.extraOidcClaimsPolicies;
scopes = cfg.extraOidcScopes;
authorization_policies = cfg.extraOidcAuthorizationPolicies;
};
}
// lib.optionalAttrs (cfg.extraDefinitions != { }) {
definitions = cfg.extraDefinitions;
}; };
settingsFiles = [ "/var/lib/authelia-${fqdn}/oidc_clients.yaml" ]; settingsFiles = [ "/var/lib/authelia-${fqdn}/oidc_clients.yaml" ];
@ -573,22 +344,16 @@ in
systemd.services."authelia-${fqdn}".preStart = systemd.services."authelia-${fqdn}".preStart =
let let
mkCfg = mkCfg = clients:
clients: shblib.replaceSecrets {
shb.replaceSecrets {
userConfig = { userConfig = {
identity_providers.oidc.clients = clients; identity_providers.oidc.clients = clients;
}; };
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml"; resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
generator = shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { }); generator = shblib.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
}; };
in in
lib.mkBefore ( lib.mkBefore (mkCfg cfg.oidcClients);
mkCfg cfg.oidcClients
+ ''
${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2'
''
);
services.nginx.virtualHosts.${fqdn} = { services.nginx.virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl); forceSSL = !(isNull cfg.ssl);
@ -609,14 +374,12 @@ in
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:9091; proxy_pass http://127.0.0.1:${toString autheliaCfg.settings.server.port};
proxy_intercept_errors on; proxy_intercept_errors on;
if ($request_method !~ ^(POST)$){ if ($request_method !~ ^(POST)$){
error_page 401 = /error/401; error_page 401 = /error/401;
@ -635,25 +398,10 @@ in
add_header X-Permitted-Cross-Domain-Policies none; add_header X-Permitted-Cross-Domain-Policies none;
proxy_set_header Host $http_x_forwarded_host; proxy_set_header Host $http_x_forwarded_host;
proxy_pass http://127.0.0.1:9091; proxy_pass http://127.0.0.1:${toString autheliaCfg.settings.server.port};
''; '';
}; };
# I would like this to live outside of the Authelia module.
# This will require a reverse proxy contract.
# Actually, not sure a full reverse proxy contract is needed.
shb.mitmdump.instances."authelia-${fqdn}" = lib.mkIf cfg.debug {
listenPort = 9091;
upstreamPort = 9090;
timeout = 30;
after = [ "authelia-${fqdn}.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set"
"verbose_pattern=/api"
];
};
services.redis.servers.authelia = { services.redis.servers.authelia = {
enable = true; enable = true;
user = autheliaCfg.user; user = autheliaCfg.user;
@ -672,29 +420,9 @@ in
static_configs = [ static_configs = [
{ {
targets = ["127.0.0.1:9959"]; targets = ["127.0.0.1:9959"];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
} }
]; ];
} }
]; ];
systemd.targets."authelia-${fqdn}" =
let
services = [
"authelia-${fqdn}.service"
]
++ lib.optionals cfg.debug [
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
];
in
{
after = services;
requires = services;
wantedBy = [ "multi-user.target" ];
};
}; };
} }

View file

@ -1,244 +0,0 @@
# Authelia Block {#blocks-authelia}
Defined in [`/modules/blocks/authelia.nix`](@REPO@/modules/blocks/authelia.nix).
This block sets up an [Authelia][] service for Single-Sign On integration.
[Authelia]: https://www.authelia.com/
Compared to the upstream nixpkgs module, this module is tightly integrated
with SHB which allows easy configuration of SSO with [OIDC integration](#blocks-authelia-shb-oidc)
as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) features.
Note that forward authentication is configured with the [nginx block](blocks-nginx.html#blocks-nginx-usage-shbforwardauth).
## Features {#services-authelia-features}
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-authelia-usage-applicationdashboard)
## Usage {#services-authelia-usage}
### Initial Configuration {#blocks-authelia-usage-configuration}
Authelia cannot work without SSL and LDAP.
So setting up the Authelia block requires to setup the [SSL block][] first
and the [LLDAP block][] first.
[SSL block]: blocks-ssl.html
[LLDAP block]: blocks-lldap.html
SSL is required to encrypt the communication and LDAP is used to handle users and group assignments.
Authelia will allow access to a given resource only if the user that is authenticated
is a member of the corresponding LDAP group.
Afterwards, assuming the LDAP service runs on the same machine,
the Authelia configuration can be done with:
```nix
shb.authelia = {
enable = true;
domain = "example.com";
subdomain = "auth";
ssl = config.shb.certs.certs.letsencrypt."example.com";
ldapHostname = "127.0.0.1";
ldapPort = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
smtp = {
host = "smtp.eu.mailgun.org";
port = 587;
username = "postmaster@mg.example.com";
from_address = "authelia@example.com";
password.result = config.shb.sops.secret."authelia/smtp_password".result;
};
secrets = {
jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result;
ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result;
sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result;
storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result;
identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result;
};
};
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ];
shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
shb.sops.secret."authelia/ldap_admin_password" = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.key = "lldap/user_password";
};
shb.sops.secret."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.sops.secret."authelia/smtp_password".request = config.shb.authelia.smtp.password.request;
```
This assumes secrets are setup with SOPS
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
It's a bit annoying to setup all those secrets but it's only necessary once.
Use `nix run nixpkgs#openssl -- rand -hex 64` to generate them.
Crucially, the `shb.authelia.secrets.ldapAdminPasswordFile` must be the same
as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][].
This is done using Sops' `key` option.
### Application Dashboard {#services-authelia-usage-applicationdashboard}
Integration with the [dashboard contract](contracts-dashboard.html) is provided
by the [dashboard option](#blocks-authelia-options-shb.authelia.dashboard).
For example using the [Homepage](services-homepage.html) service:
```nix
{
shb.homepage.servicesGroups.Admin.services.Authelia = {
sortOrder = 2;
dashboard.request = config.shb.authelia.dashboard.request;
};
}
```
## SHB OIDC integration {#blocks-authelia-shb-oidc}
For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC],
integrating with this block is done by configuring the service itself
and linking it to this Authelia block through the `endpoint` option
and by sharing a secret:
[services]: services.html
[OIDC]: https://openid.net/developers/how-connect-works/
```nix
shb.<service>.sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secret.result = config.shb.sops.secret."<service>/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."<service>/sso/secretForAuthelia".result;
};
shb.sops.secret."<service>/sso/secret".request = config.shb.<service>.sso.secret.request;
shb.sops.secret."<service>/sso/secretForAuthelia" = {
request = config.shb.<service>.sso.secretForAuthelia.request;
settings.key = "<service>/sso/secret";
};
```
To share a secret between the service and Authelia,
we generate a secret with `nix run nixpkgs#openssl -- rand -hex 64` under `<service>/sso/secret`
then we ask Sops to use the same password for `<service>/sso/secretForAuthelia`
thanks to the `settings.key` option.
The difference between both secrets is one if owned by the `authelia` user
while the other is owned by the user of the `<service`> we are configuring.
## OIDC Integration {#blocks-authelia-oidc}
To integrate a service handling OIDC integration not provided by SelfHostBlocks with this Authelia block,
the necessary configuration is:
```nix
shb.authelia.oidcClients = [
{
client_id = "<service>";
client_secret.source = config.shb.sops.secret."<service>/sso/secretForAuthelia".response.path;
scopes = [ "openid" "email" "profile" ];
redirect_uris = [
"<provided by service documentation>"
];
}
];
shb.sops.secret."<service>/sso/secret".request = {
owner = "<service_user>";
};
shb.sops.secret."<service>/sso/secretForAuthelia" = {
request.owner = "authelia";
settings.key = "<service>/sso/secret";
};
```
As in the previous section, we create a shared secret using Sops'
`settings.key` option.
The configuration for the service itself is much dependent on the service itself.
For example for [open-webui][], the configuration looks like so:
[open-webui]: https://search.nixos.org/options?query=services.open-webui
```nix
services.open-webui.environment = {
ENABLE_SIGNUP = "False";
WEBUI_AUTH = "True";
ENABLE_FORWARD_USER_INFO_HEADERS = "True";
ENABLE_OAUTH_SIGNUP = "True";
OAUTH_UPDATE_PICTURE_ON_LOGIN = "True";
OAUTH_CLIENT_ID = "open-webui";
OAUTH_CLIENT_SECRET = "<raw secret>";
OPENID_PROVIDER_URL = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}/.well-known/openid-configuration";
OAUTH_PROVIDER_NAME = "Single Sign-On";
OAUTH_SCOPES = "openid email profile";
OAUTH_ALLOWED_ROLES = "open-webui_user";
OAUTH_ADMIN_ROLES = "open-webui_admin";
ENABLE_OAUTH_ROLE_MANAGEMENT = "True";
};
shb.authelia.oidcClients = [
{
client_id = "open-webui";
client_secret.source = config.shb.sops.secret."open-webui/sso/secretForAuthelia".response.path;
scopes = [ "openid" "email" "profile" ];
redirect_uris = [
"<provided by service documentation>"
];
}
];
shb.sops.secret."open-webui/sso/secret".request = {
owner = "open-webui";
};
shb.sops.secret."open-webui/sso/secretForAuthelia" = {
request.owner = "authelia";
settings.key = "open-webui/sso/secret";
};
```
Here, there is no way to give a path for the `OAUTH_CLIENT_SECRET`,
we are obligated to pass the raw secret which is a very bad idea.
There are ways around this but they are out of scope for this section.
Inspiration can be taken from SelfHostBlocks' source code.
To access the UI, we will need to create an `open-webui_user` and
`open-webui_admin` LDAP group and assign our user to it.
## Forward Auth {#blocks-authelia-forward-auth}
Forward authentication is provided by the [nginx block](blocks-nginx.html#blocks-nginx-usage-ssl).
## Troubleshooting {#blocks-authelia-troubleshooting}
Set the [debug][opt-debug] option to `true` to:
[opt-debug]: #blocks-authelia-options-shb.authelia.debug
- Set logging level to `"debug"`.
- Add an [shb.mitmdump][] instance in front of Authelia
which prints all requests and responses headers and body
to the systemd service `mitmdump-authelia-${config.shb.authelia.subdomain}.${config.shb.authelia.domain}.service`.
[shb.mitmdump]: ./blocks-mitmdump.html
## Tests {#blocks-authelia-tests}
Specific integration tests are defined in [`/test/blocks/authelia.nix`](@REPO@/test/blocks/authelia.nix).
## Options Reference {#blocks-authelia-options}
```{=include=} options
id-prefix: blocks-authelia-options-
list-id: selfhostblocks-block-authelia-options
source: @OPTIONS_JSON@
```

352
modules/blocks/backup.nix Normal file
View file

@ -0,0 +1,352 @@
{ config, pkgs, lib, utils, ... }:
let
cfg = config.shb.backup;
instanceOptions = {
enable = lib.mkEnableOption "shb backup instance";
backend = lib.mkOption {
description = "What program to use to make the backups.";
type = lib.types.enum [ "borgmatic" "restic" ];
example = "borgmatic";
};
keySopsFile = lib.mkOption {
description = "Sops file that holds this instance's Borgmatic repository key and passphrase.";
type = lib.types.path;
example = "secrets/backup.yaml";
};
sourceDirectories = lib.mkOption {
description = "Borgmatic source directories.";
type = lib.types.nonEmptyListOf lib.types.str;
};
excludePatterns = lib.mkOption {
description = "Borgmatic exclude patterns.";
type = lib.types.listOf lib.types.str;
default = [];
};
secretName = lib.mkOption {
description = "Secret name, if null use the name of the backup instance.";
type = lib.types.nullOr lib.types.str;
default = null;
};
repositories = lib.mkOption {
description = "Repositories to back this instance to.";
type = lib.types.nonEmptyListOf (lib.types.submodule {
options = {
path = lib.mkOption {
type = lib.types.str;
description = "Repository location";
};
timerConfig = lib.mkOption {
type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
};
});
};
retention = lib.mkOption {
description = "Retention options.";
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.int lib.types.nonEmptyStr ]);
default = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
consistency = lib.mkOption {
description = "Consistency frequency options. Only applicable for borgmatic";
type = lib.types.attrsOf lib.types.nonEmptyStr;
default = {};
example = {
repository = "2 weeks";
archives = "1 month";
};
};
hooks = lib.mkOption {
description = "Borgmatic hooks.";
default = {};
type = lib.types.submodule {
options = {
before_backup = lib.mkOption {
description = "Hooks to run before backup";
type = lib.types.listOf lib.types.str;
default = [];
};
after_backup = lib.mkOption {
description = "Hooks to run after backup";
type = lib.types.listOf lib.types.str;
default = [];
};
};
};
};
environmentFile = lib.mkOption {
type = lib.types.bool;
description = "Add environment file to be read by the systemd service.";
default = false;
example = true;
};
};
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (lib.strings.removePrefix "/" name);
in
{
options.shb.backup = {
onlyOnAC = lib.mkOption {
description = "Run backups only if AC power is plugged in.";
default = true;
example = false;
type = lib.types.bool;
};
user = lib.mkOption {
description = "Unix user doing the backups.";
type = lib.types.str;
default = "backup";
};
group = lib.mkOption {
description = "Unix group doing the backups.";
type = lib.types.str;
default = "backup";
};
instances = lib.mkOption {
description = "Each instance is a backup setting";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = instanceOptions;
});
};
borgServer = lib.mkOption {
description = "Add borgbackup package so external backups can use this server as a remote.";
default = false;
example = true;
type = lib.types.bool;
};
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = lib.mkOption {
description = "Reduce performance impact of backup jobs.";
default = {};
type = lib.types.submodule {
options = {
niceness = lib.mkOption {
type = lib.types.ints.between (-20) 19;
description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process";
default = 15;
};
ioSchedulingClass = lib.mkOption {
type = lib.types.enum [ "idle" "best-effort" "realtime" ];
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = "best-effort";
};
ioPriority = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 0 7);
description = "ionice priority, defaults to 7 for lowest priority IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = 7;
};
};
};
};
};
config = lib.mkIf (cfg.instances != {}) (
let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
borgmaticInstances = lib.attrsets.filterAttrs (k: i: i.backend == "borgmatic") enabledInstances;
resticInstances = lib.attrsets.filterAttrs (k: i: i.backend == "restic") enabledInstances;
in lib.mkMerge [
# Secrets configuration
{
users.users = {
${cfg.user} = {
name = cfg.user;
group = cfg.group;
home = "/var/lib/backup";
createHome = true;
isSystemUser = true;
extraGroups = [ "keys" ];
};
};
users.groups = {
${cfg.group} = {
name = cfg.group;
};
};
sops.secrets =
let
mkSopsSecret = name: instance: (
[
{
"${instance.backend}/passphrases/${if isNull instance.secretName then name else instance.secretName}" = {
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}
] ++ lib.optional ((lib.filter ({path, ...}: lib.strings.hasPrefix "s3" path) instance.repositories) != []) {
"${instance.backend}/environmentfiles/${if isNull instance.secretName then name else instance.secretName}" = {
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
} ++ lib.optionals (instance.backend == "borgmatic") (lib.flatten (map ({path, ...}: {
"${instance.backend}/keys/${repoSlugName path}" = {
key = "${instance.backend}/keys/${if isNull instance.secretName then name else instance.secretName}";
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}) instance.repositories))
);
in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
}
# Borgmatic configuration
{
systemd.timers.borgmatic = lib.mkIf (borgmaticInstances != {}) {
timerConfig = {
OnCalendar = "hourly";
};
};
systemd.services.borgmatic = lib.mkIf (borgmaticInstances != {}) {
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStartPre = [ "" ]; # Do not sleep before starting.
ExecStart = [ "" "${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1" ];
# For borgmatic, since we have only one service, we need to merge all environmentFile
# from all instances.
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
};
};
systemd.packages = lib.mkIf (borgmaticInstances != {}) [ pkgs.borgmatic ];
environment.systemPackages = (
lib.optionals cfg.borgServer [ pkgs.borgbackup ]
++ lib.optionals (borgmaticInstances != {}) [ pkgs.borgbackup pkgs.borgmatic ]
);
environment.etc =
let
mkSettings = name: instance: {
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML {} {
location =
{
source_directories = instance.sourceDirectories;
repositories = map ({path, ...}: path) instance.repositories;
}
// (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
excludePatterns = instance.excludePatterns;
});
storage = {
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${if isNull instance.secretName then name else instance.secretName}";
borg_keys_directory = "/run/secrets/borgmatic/keys";
};
retention = instance.retention;
consistency.checks =
let
mkCheck = name: frequency: {
inherit name frequency;
};
in
lib.attrsets.mapAttrsToList mkCheck instance.consistency;
# hooks = lib.mkMerge [
# lib.optionalAttrs (builtins.length instance.hooks.before_backup > 0) {
# inherit (instance.hooks) before_backup;
# }
# lib.optionalAttrs (builtins.length instance.hooks.after_backup > 0) {
# inherit (instance.hooks) after_backup;
# }
# ];
};
};
in
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings borgmaticInstances);
}
# Restic configuration
{
environment.systemPackages = lib.optionals (resticInstances != {}) [ pkgs.restic ];
services.restic.backups =
let
mkRepositorySettings = name: instance: repository: {
"${name}_${repoSlugName repository.path}" = {
inherit (cfg) user;
repository = repository.path;
paths = instance.sourceDirectories;
passwordFile = "/run/secrets/${instance.backend}/passphrases/${name}";
initialize = true;
inherit (repository) timerConfig;
pruneOpts = lib.mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
) instance.retention;
backupPrepareCommand = lib.strings.concatStringsSep "\n" instance.hooks.before_backup;
backupCleanupCommand = lib.strings.concatStringsSep "\n" instance.hooks.after_backup;
} // lib.attrsets.optionalAttrs (instance.environmentFile) {
environmentFile = "/run/secrets/${instance.backend}/environmentfiles/${name}";
} // lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
exclude = instance.excludePatterns;
};
};
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories;
in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSettings resticInstances));
systemd.services =
let
mkRepositorySettings = name: instance: repository: {
"restic-backups-${name}_${repoSlugName repository.path}".serviceConfig = {
Nice = cfg.performance.niceness;
IOSchedulingClass = cfg.performance.ioSchedulingClass;
IOSchedulingPriority = cfg.performance.ioPriority;
};
};
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories;
in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSettings resticInstances));
}
]);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,230 @@
# Backup Block {#blocks-backup}
Defined in [`/modules/blocks/backup.nix`](@REPO@/modules/blocks/backup.nix).
This block sets up backup jobs for Self Host Blocks.
## Features {#blocks-backup-features}
Two implementations for this block are provided:
- [Restic](https://restic.net/)
- [Borgmatic](https://torsion.org/borgmatic/)
No integration tests are provided yet.
## Usage {#blocks-backup-usage}
### One folder backed up to mounted hard drives {#blocks-backup-usage-one}
The following snippet shows how to configure backup of 1 folder using the Restic implementation to 1
repository.
Assumptions:
- 1 hard drive pool is used for backup and is mounted on `/srv/pool1`.
```nix
shb.backup.instances.myfolder = {
enable = true;
backend = "restic";
keySopsFile = ./secrets.yaml;
repositories = [{
path = "/srv/pool1/backups/myfolder";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}];
sourceDirectories = [
"/var/lib/myfolder"
];
retention = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
consistency = {
repository = "2 weeks";
archives = "1 month";
};
};
```
The referenced Sops file must follow this structure:
```yaml
restic:
passphrases:
myfolder: <secret>
```
To generate a secret, use: `nix run nixpkgs#openssl -- rand -hex 64`.
With the borgmatic implementation, the structure should be:
```yaml
borgmatic:
keys:
myfolder: |
BORG_KEY <key>
passphrases:
myfolder: <secret>
```
You can have both borgmatic and restic implementations working at the same time.
### One folder backed up to S3 {#blocks-backup-usage-remote}
> This is only supported by the Restic implementation.
Here we will only highlight the differences with the previous configuration.
This assumes you have access to such a remote S3 store, for example by using Backblaze.
```diff
shb.backup.instances.myfolder = {
repositories = [{
- path = "/srv/pool1/backups/myfolder";
+ path = "s3:s3.us-west-000.backblazeb2.com/backups/myfolder";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}];
+ environmentFile = true; # Needed for s3
}
```
The Sops file has a new required field:
```yaml
restic:
passphrases:
myfolder: <secret>
+ environmentfiles:
+ myfolder: |-
+ AWS_ACCESS_KEY_ID=<aws_key_id>
+ AWS_SECRET_ACCESS_KEY=<aws_secret_key>
```
### Multiple folder to multiple destinations {#blocks-backup-usage-multiple}
The following snippet shows how to configure backup of any number of folders using the Restic
implementation to 3 repositories, each happening at different times to avoid contending for I/O
time.
We will also make sure to be able to re-use as much as the configuration as possible.
A few assumptions:
- 2 hard drive pools used for backup are mounted respectively on `/srv/pool1` and `/srv/pool2`.
- You have a backblaze account.
First, let's define a variable to hold all our repositories you want to back up to:
```nix
repos = [
{
path = "/srv/pool1/backups";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "/srv/pool2/backups";
timerConfig = {
OnCalendar = "08:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "s3:s3.us-west-000.backblazeb2.com/backups";
timerConfig = {
OnCalendar = "16:00:00";
RandomizedDelaySec = "3h";
};
}
];
```
Compared to the previous examples, we do not include the name of what we will back up in the
repository paths.
Now, let's define a function to create a backup configuration. It will take a list of repositories,
a name identifying the backup and a list of folders to back up.
```nix
backupcfg = repositories: name: sourceDirectories {
enable = true;
backend = "restic";
keySopsFile = ../secrets/backup.yaml;
repositories = builtins.map (r: {
path = "${r.path}/${name}";
inherit (r) timerConfig;
}) repositories;
inherit sourceDirectories;
retention = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
consistency = {
repository = "2 weeks";
archives = "1 month";
};
environmentFile = true;
};
```
Now, we can define multiple backup jobs to backup different folders:
```nix
shb.backup.instances.myfolder1 = backupcfg repos ["/var/lib/myfolder1"];
shb.backup.instances.myfolder2 = backupcfg repos ["/var/lib/myfolder2"];
```
The difference between the above snippet and putting all the folders into one configuration (shown
below) is the former splits the backups into sub-folders on the repositories.
```nix
shb.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/myfolder2"];
```
## Demo {#blocks-backup-demo}
[WIP]
## Monitoring {#blocks-backup-monitoring}
[WIP]
## Maintenance {#blocks-backup-maintenance}
[WIP]
## Options Reference {#blocks-backup-options}
```{=include=} options
id-prefix: blocks-backup-options-
list-id: selfhostblocks-block-backup-options
source: @OPTIONS_JSON@
```

View file

@ -1,501 +0,0 @@
{
config,
pkgs,
lib,
utils,
shb,
...
}:
let
cfg = config.shb.borgbackup;
inherit (lib)
concatStringsSep
filterAttrs
flatten
literalExpression
optionals
listToAttrs
mapAttrsToList
mkOption
mkMerge
;
inherit (lib)
mkIf
nameValuePair
optionalAttrs
removePrefix
;
inherit (lib.types)
attrsOf
int
oneOf
nonEmptyStr
nullOr
str
submodule
;
commonOptions =
{
name,
prefix,
config,
...
}:
{
enable = lib.mkEnableOption ''
SelfHostBlocks' BorgBackup block;
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
'';
passphrase = lib.mkOption {
description = "Encryption key for the backup repository.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = config.request.user;
ownerText = "[shb.borgbackup.${prefix}.<name>.request.user](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.request.user)";
restartUnits = [ (fullName name config.settings.repository) ];
restartUnitsText = "[ [shb.borgbackup.${prefix}.<name>.settings.repository](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.settings.repository) ]";
};
};
};
repository = lib.mkOption {
description = "Repository to send the backups to.";
type = submodule {
options = {
path = mkOption {
type = str;
description = "Repository location";
};
secrets = mkOption {
type = attrsOf shb.secretFileType;
default = { };
description = ''
Secrets needed to access the repository where the backups will be stored.
See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
'';
example = literalExpression ''
{
AWS_ACCESS_KEY_ID.source = <path/to/secret>;
AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
}
'';
};
timerConfig = mkOption {
type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
};
description = "When to run the backup. See {manpage}`systemd.timer(5)` for details.";
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
};
};
};
retention = lib.mkOption {
description = "Retention options. See {command}`borg help prune` for the available options.";
type = attrsOf (oneOf [
int
nonEmptyStr
]);
default = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
consistency = lib.mkOption {
description = "Consistency frequency options.";
type = lib.types.attrsOf lib.types.nonEmptyStr;
default = { };
example = {
repository = "2 weeks";
archives = "1 month";
};
};
limitUploadKiBs = mkOption {
type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
stateDir = mkOption {
type = nullOr lib.types.str;
description = ''
Override the directory in which {command}`borg` stores its
configuration and cache. By default it uses the user's
home directory but is some cases this can cause conflicts.
'';
default = null;
};
};
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
fullName = name: repository: "borgbackup-job-${name}_${repoSlugName repository.path}";
in
{
imports = [
../../lib/module.nix
../blocks/monitoring.nix
];
options.shb.borgbackup = {
enableDashboard = lib.mkEnableOption "the Backups SHB dashboard" // {
default = true;
};
instances = mkOption {
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = shb.contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the BorgBackup provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "instances";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
databases = mkOption {
description = "Databases to backup following the [database backup contract](./shb.contracts-databasebackup.html).";
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = shb.contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the BorgBackup provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "databases";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
borgServer = lib.mkOption {
description = "Add borgbackup package to `environment.systemPackages` so external backups can use this server as a remote.";
default = false;
example = true;
type = lib.types.bool;
};
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = lib.mkOption {
description = "Reduce performance impact of backup jobs.";
default = { };
type = lib.types.submodule {
options = {
niceness = lib.mkOption {
type = lib.types.ints.between (-20) 19;
description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process";
default = 15;
};
ioSchedulingClass = lib.mkOption {
type = lib.types.enum [
"idle"
"best-effort"
"realtime"
];
description = "ionice scheduling class, defaults to best-effort IO.";
default = "best-effort";
};
ioPriority = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 0 7);
description = "ionice priority, defaults to 7 for lowest priority IO.";
default = 7;
};
};
};
};
};
config = lib.mkIf (cfg.instances != { } || cfg.databases != { }) (
let
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
in
lib.mkMerge [
{
environment.systemPackages =
optionals (cfg.borgServer || enabledInstances != { } || enabledDatabases != { })
[
pkgs.borgbackup
];
}
{
services.borgbackup.jobs =
let
mkJob = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
repo = instance.settings.repository.path;
paths = instance.request.sourceDirectories;
encryption.mode = "repokey-blake2";
# We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";
doInit = true;
failOnWarnings = true;
stateDir = instance.settings.stateDir;
persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
startAt = ""; # Some non-empty string value tricks the upstream module in creating the systemd timer.
prune.keep = instance.settings.retention;
preHook = concatStringsSep "\n" instance.request.hooks.beforeBackup;
postHook = concatStringsSep "\n" instance.request.hooks.afterBackup;
extraArgs = (
optionals (instance.settings.limitUploadKiBs != null) [
"--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
]
);
exclude = instance.request.excludePatterns;
};
};
in
mkMerge (mapAttrsToList mkJob enabledInstances);
}
{
services.borgbackup.jobs =
let
mkJob = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
repo = instance.settings.repository.path;
dumpCommand = lib.getExe (
pkgs.writeShellApplication {
name = "dump-command";
text = instance.request.backupCmd;
}
);
encryption.mode = "repokey-blake2";
# We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";
doInit = true;
failOnWarnings = true;
stateDir = instance.settings.stateDir;
persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
startAt = ""; # Some non-empty list value that tricks upstream in creating the systemd timer.
prune.keep = instance.settings.retention;
extraArgs = (
optionals (instance.settings.limitUploadKiBs != null) [
"--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
]
);
};
};
in
mkMerge (mapAttrsToList mkJob enabledDatabases);
}
{
systemd.timers =
let
mkTimer = name: instance: {
${fullName name instance.settings.repository} = {
timerConfig = lib.mkForce instance.settings.repository.timerConfig;
};
};
in
mkMerge (mapAttrsToList mkTimer (enabledInstances // enabledDatabases));
}
{
systemd.services =
let
mkSettings =
name: instance:
let
serviceName = fullName name instance.settings.repository;
in
{
${serviceName} = mkMerge [
{
serviceConfig = {
# Purposely not a oneshot systemd service otherwise
# the service waits on the completion the backup before deactivating.
# This seems like a nice property at first but there is one annoying
# edge case when deploying. If a backup job somehow is started when
# the deploy happens, the deploy will wait on the service to finish
# before considering the deploy done. And worse, it will consider the
# deploy as failed if the backup fails, which is not what you want.
Nice = lib.mkForce cfg.performance.niceness;
IOSchedulingClass = lib.mkForce cfg.performance.ioSchedulingClass;
IOSchedulingPriority = lib.mkForce cfg.performance.ioPriority;
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != { }) {
serviceConfig.EnvironmentFile = [
"/run/secrets_borgbackup/${serviceName}"
];
after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ];
})
];
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
let
script = shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_borgbackup/${serviceName}";
generator = shb.toEnvVar;
user = instance.request.user;
};
in
{
script = script.preStart;
# Makes the systemd service wait for the backup to be done before changing state to inactive.
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
}
);
};
in
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
}
{
systemd.services =
let
mkEnv =
name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = (
shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
BORG_PASSCOMMAND = ''"cat ${instance.settings.passphrase.result.path}"'';
BORG_REPO = instance.settings.repository.path;
};
resultPath = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
generator = shb.toEnvVar;
user = instance.request.user;
}
);
};
in
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
}
{
environment.systemPackages =
let
mkBorgBackupBinary =
name: instance:
shb.contracts.backup.mkRestoreScript {
name = fullName name instance.settings.repository;
user = instance.request.user;
sudoPreserveEnvs = [
"BORG_REPO"
"BORG_PASSCOMMAND"
];
secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
restoreCmd = ''(cd / && ${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\")'';
listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
};
in
flatten (mapAttrsToList mkBorgBackupBinary cfg.instances);
}
{
environment.systemPackages =
let
mkBorgBackupBinary =
name: instance:
shb.contracts.backup.mkRestoreScript {
name = fullName name instance.settings.repository;
user = instance.request.user;
sudoPreserveEnvs = [
"BORG_REPO"
"BORG_PASSCOMMAND"
];
secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
restoreCmd = ''${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\" --stdout | ${instance.request.restoreCmd}'';
listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
};
in
flatten (mapAttrsToList mkBorgBackupBinary cfg.databases);
}
(lib.mkIf (cfg.enableDashboard && (cfg.instances != { } || cfg.databases != { })) {
shb.monitoring.dashboards = [
./backup/dashboard/Backups.json
];
})
]
);
}

View file

@ -1,302 +0,0 @@
# Borgbackup Block {#blocks-borgbackup}
Defined in [`/modules/blocks/borgbackup.nix`](@REPO@/modules/blocks/borgbackup.nix).
This block sets up a backup job using [BorgBackup][].
It is heavily based on the nixpkgs BorgBackup module.
[borgbackup]: https://www.borgbackup.org/
## Provider Contracts {#blocks-borgbackup-contract-provider}
This block provides the following contracts:
- [backup contract](contracts-backup.html) under the [`shb.borgbackup.instances`][instances] option.
It is tested with [contract tests][backup contract tests].
- [database backup contract](contracts-databasebackup.html) under the [`shb.borgbackup.databases`][databases] option.
It is tested with [contract tests][database backup contract tests].
[instances]: #blocks-borgbackup-options-shb.borgbackup.instances
[databases]: #blocks-borgbackup-options-shb.borgbackup.databases
[backup contract tests]: @REPO@/test/contracts/backup.nix
[database backup contract tests]: @REPO@/test/contracts/databasebackup.nix
As requested by those two contracts, when setting up a backup with BorgBackup,
a backup Systemd service and a [restore script](#blocks-borgbackup-maintenance) are provided.
## Usage {#blocks-borgbackup-usage}
The following examples assume usage of the [sops block][] to provide secrets
although any blocks providing the [secrets contract][] works too.
[sops block]: ./blocks-sops.html
[secrets contract]: ./contracts-secrets.html
### One folder backed up manually {#blocks-borgbackup-usage-provider-manual}
The following snippet shows how to configure
the backup of 1 folder to 1 repository.
We assume that the folder `/var/lib/myfolder` of the service `myservice` must be backed up.
```nix
shb.borgbackup.instances."myservice" = {
request = {
user = "myservice";
sourceDirectories = [
"/var/lib/myfolder"
];
};
settings = {
enable = true;
passphrase.result = config.shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
config.shb.borgbackup.instances."myservice".settings.passphrase.request;
```
### One folder backed up with contract {#blocks-borgbackup-usage-provider-contract}
With the same example as before but assuming the `myservice` service
has a `myservice.backup` option that is a requester for the backup contract,
the snippet above becomes:
```nix
shb.borgbackup.instances."myservice" = {
request = config.myservice.backup;
settings = {
enable = true;
passphrase.result = config.shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
config.shb.borgbackup.instances."myservice".settings.passphrase.request;
```
### One folder backed up to S3 {#blocks-borgbackup-usage-provider-remote}
Here we will only highlight the differences with the previous configuration.
This assumes you have access to such a remote S3 store, for example by using [Backblaze](https://www.backblaze.com/).
```diff
shb.test.backup.instances.myservice = {
repository = {
- path = "/srv/pool1/backups/myfolder";
+ path = "s3:s3.us-west-000.backblazeb2.com/backups/myfolder";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
+ extraSecrets = {
+ AWS_ACCESS_KEY_ID.source="<path/to/access_key_id>";
+ AWS_SECRET_ACCESS_KEY.source="<path/to/secret_access_key>";
+ };
};
}
```
### Multiple directories to multiple destinations {#blocks-borgbackup-usage-multiple}
The following snippet shows how to configure backup of any number of folders to 3 repositories,
each happening at different times to avoid I/O contention.
We will also make sure to be able to re-use as much as the configuration as possible.
A few assumptions:
- 2 hard drive pools used for backup are mounted respectively on `/srv/pool1` and `/srv/pool2`.
- You have a backblaze account.
First, let's define a variable to hold all the repositories we want to back up to:
```nix
repos = [
{
path = "/srv/pool1/backups";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "/srv/pool2/backups";
timerConfig = {
OnCalendar = "08:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "s3:s3.us-west-000.backblazeb2.com/backups";
timerConfig = {
OnCalendar = "16:00:00";
RandomizedDelaySec = "3h";
};
}
];
```
Compared to the previous examples, we do not include the name of what we will back up in the
repository paths.
Now, let's define a function to create a backup configuration. It will take a list of repositories,
a name identifying the backup and a list of folders to back up.
```nix
backupcfg = repositories: name: sourceDirectories {
enable = true;
backend = "borgbackup";
keySopsFile = ../secrets/backup.yaml;
repositories = builtins.map (r: {
path = "${r.path}/${name}";
inherit (r) timerConfig;
}) repositories;
inherit sourceDirectories;
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
environmentFile = true;
};
```
Now, we can define multiple backup jobs to backup different folders:
```nix
shb.test.backup.instances.myfolder1 = backupcfg repos ["/var/lib/myfolder1"];
shb.test.backup.instances.myfolder2 = backupcfg repos ["/var/lib/myfolder2"];
```
The difference between the above snippet and putting all the folders into one configuration (shown
below) is the former splits the backups into sub-folders on the repositories.
```nix
shb.test.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/myfolder2"];
```
## Monitoring {#blocks-borgbackup-monitoring}
A generic dashboard for all backup solutions is provided.
See [Backups Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-backup) section in the monitoring chapter.
## Maintenance {#blocks-borgbackup-maintenance}
### Manual Backup {#blocks-borgbackup-maintenance-manuql}
To launch a backup manually, just run:
```bash
systemctl start <systemd-service-name>
```
You can easily discover the systemd service name you need by either listing the units:
```bash
systemctl list-units 'borgbackup*'
```
Or by autocompleting the unit name with `<TAB>`:
```bash
systemctl start borgbackup<TAB><TAB>
```
Note that the systemd services are of `Type=simple` which means the systemd service
will not wait for the backup completion to terminate.
If you want instead to wait for the backup to complete, use the `--wait` flag:
```bash
systemctl start --wait <systemd-service-name>
```
### Restore {#blocks-borgbackup-maintenance-restore}
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
The restore script has all the secrets needed to access the repo,
it will run `sudo` automatically
and the user running it needs to have correct permissions for privilege escalation
In the [multiple directories example](#blocks-borgbackup-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
```bash
borgbackup-job-myfolder1_srv_pool1_backups
borgbackup-job-myfolder1_srv_pool2_backups
borgbackup-job-myfolder1_s3_s3.us-west-000.backblazeb2.com_backups
borgbackup-job-myfolder2_srv_pool1_backups
borgbackup-job-myfolder2_srv_pool2_backups
borgbackup-job-myfolder2_s3_s3.us-west-000.backblazeb2.com_backups
```
Discovering those is easy thanks to tab-completion.
One can then restore a backup from a given repository with:
```bash
borgbackup-job-myfolder1_srv_pool1_backups restore latest
```
### Troubleshooting {#blocks-borgbackup-maintenance-troubleshooting}
In case something bad happens with a backup, the [official documentation](https://borgbackup.readthedocs.io/en/stable/077_troubleshooting.html) has a lot of tips.
## Tests {#blocks-borgbackup-tests}
Specific integration tests are defined in [`/test/blocks/borgbackup.nix`](@REPO@/test/blocks/borgbackup.nix).
## Options Reference {#blocks-borgbackup-options}
```{=include=} options
id-prefix: blocks-borgbackup-options-
list-id: selfhostblocks-block-borgbackup-options
source: @OPTIONS_JSON@
```

View file

@ -1,15 +1,16 @@
{ config, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.shb.davfs; cfg = config.shb.davfs;
shblib = pkgs.callPackage ../../lib {};
in in
{ {
options.shb.davfs = { options.shb.davfs = {
mounts = lib.mkOption { mounts = lib.mkOption {
description = "List of mounts."; description = "List of mounts.";
default = []; default = [];
type = lib.types.listOf ( type = lib.types.listOf (lib.types.submodule {
lib.types.submodule {
options = { options = {
remoteUrl = lib.mkOption { remoteUrl = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -67,38 +68,13 @@ in
default = true; default = true;
}; };
}; };
} });
);
}; };
}; };
config = { config = {
services.davfs2.enable = builtins.length cfg.mounts > 0; services.davfs2.enable = builtins.length cfg.mounts > 0;
systemd.services = lib.optionalAttrs (builtins.length cfg.mounts > 0) {
davfs2-password-generation = {
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /etc/davfs2
[ -f /etc/davfs2/secrets ] && mv /etc/davfs2/secrets /etc/davfs2/secrets.prev
touch /etc/davfs2/secrets
chown root: /etc/davfs2
chown root: /etc/davfs2/secrets
chmod 700 /etc/davfs2
chmod 600 /etc/davfs2/secrets
''
+ (
let
mkPasswordCmd = cfg': ''
printf "%s %s %s\n" "${cfg'.mountPoint}" "${cfg'.username}" "$(cat ${cfg'.passwordFile})" >> /etc/davfs2/secrets
'';
in
lib.concatStringsSep "\n" (map mkPasswordCmd cfg.mounts)
);
};
};
systemd.mounts = systemd.mounts =
let let
mkMountCfg = c: { mkMountCfg = c: {
@ -106,13 +82,12 @@ in
description = "Webdav mount point"; description = "Webdav mount point";
after = [ "network-online.target" ]; after = [ "network-online.target" ];
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
what = c.remoteUrl; what = c.remoteUrl;
where = c.mountPoint; where = c.mountPoint;
options = lib.concatStringsSep "," ( options = lib.concatStringsSep "," (
(lib.optional (!(isNull c.uid)) "uid=${toString c.uid}") (lib.optional (!(isNull c.uid)) "uid=${toString c.uid}")
++ (lib.optional (!(isNull c.gid)) "gid=${toString c.gid}") ++ (lib.optional (!(isNull c.gid)) "gid=${toString c.uid}")
++ (lib.optional (!(isNull c.fileMode)) "file_mode=${toString c.fileMode}") ++ (lib.optional (!(isNull c.fileMode)) "file_mode=${toString c.fileMode}")
++ (lib.optional (!(isNull c.directoryMode)) "dir_mode=${toString c.directoryMode}") ++ (lib.optional (!(isNull c.directoryMode)) "dir_mode=${toString c.directoryMode}")
); );

View file

@ -1,105 +0,0 @@
{
config,
lib,
pkgs,
shb,
...
}:
let
cfg = config.shb.hardcodedsecret;
inherit (lib) mapAttrs' mkOption nameValuePair;
inherit (lib.types)
attrsOf
nullOr
str
submodule
;
inherit (pkgs) writeText;
in
{
imports = [
../../lib/module.nix
];
options.shb.hardcodedsecret = mkOption {
default = { };
description = ''
Hardcoded secrets. These should only be used in tests.
'';
example = lib.literalExpression ''
{
mySecret = {
request = {
user = "me";
mode = "0400";
restartUnits = [ "myservice.service" ];
};
settings.content = "My Secret";
};
}
'';
type = attrsOf (
submodule (
{ name, ... }:
{
options = shb.contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the hardcoded secret module.
Give either `content` or `source`.
'';
type = submodule {
options = {
content = mkOption {
type = nullOr str;
description = ''
Content of the secret as a string.
This will be stored in the nix store and should only be used for testing or maybe in dev.
'';
default = null;
};
source = mkOption {
type = nullOr str;
description = ''
Source of the content of the secret as a path in the nix store.
'';
default = null;
};
};
};
};
resultCfg = {
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
};
};
}
)
);
};
config = {
system.activationScripts = mapAttrs' (
n: cfg':
let
source =
if cfg'.settings.source != null then
cfg'.settings.source
else
writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
in
nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.result.path}")"
touch "${cfg'.result.path}"
chmod ${cfg'.request.mode} "${cfg'.result.path}"
chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
cp ${source} "${cfg'.result.path}"
''
) cfg;
};
}

303
modules/blocks/ldap.nix Normal file
View file

@ -0,0 +1,303 @@
{ config, pkgs, lib, ... }:
let
cfg = config.shb.ldap;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}";
lldap-cli-auth = pkgs.callPackage ({ stdenvNoCC, makeWrapper, lldap-cli }: stdenvNoCC.mkDerivation {
name = "lldap-cli";
src = lldap-cli;
nativeBuildInputs = [
makeWrapper
];
# No quotes around the value for LLDAP_PASSWORD because we want the value to not be enclosed in quotes.
installPhase = ''
makeWrapper ${pkgs.lldap-cli}/bin/lldap-cli $out/bin/lldap-cli \
--set LLDAP_USERNAME "admin" \
--set LLDAP_PASSWORD $(cat ${cfg.ldapUserPasswordFile}) \
--set LLDAP_HTTPURL "http://${config.services.lldap.settings.http_host}:${toString config.services.lldap.settings.http_port}"
'';
}) {};
in
{
options.shb.ldap = {
enable = lib.mkEnableOption "the LDAP service";
dcdomain = lib.mkOption {
type = lib.types.str;
description = "dc domain to serve.";
example = "dc=mydomain,dc=com";
};
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which the LDAP service will be served.";
example = "grafana";
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain under which the LDAP service will be served.";
example = "mydomain.com";
};
ldapPort = lib.mkOption {
type = lib.types.port;
description = "Port on which the server listens for the LDAP protocol.";
default = 3890;
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
webUIListenPort = lib.mkOption {
type = lib.types.port;
description = "Port on which the web UI is exposed.";
default = 17170;
};
ldapUserPasswordFile = lib.mkOption {
type = lib.types.path;
description = "File containing the LDAP admin user password.";
};
jwtSecretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the JWT secret.";
};
restrictAccessIPRange = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Set a local network range to restrict access to the UI to only those IPs.";
example = "192.168.1.1/24";
default = null;
};
debug = lib.mkOption {
description = "Enable debug logging.";
type = lib.types.bool;
default = false;
};
groups = lib.mkOption {
description = "LDAP Groups to manage declaratively.";
default = {};
example = lib.literalExpression ''
{
family = {};
}
'';
type = lib.types.attrsOf (lib.types.submodule {
options = {};
});
};
users = lib.mkOption {
description = "LDAP Users to manage declaratively.";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = {
email = lib.mkOption {
description = "Email address.";
type = lib.types.str;
};
displayName = lib.mkOption {
description = "Display name.";
type = lib.types.str;
};
firstName = lib.mkOption {
description = "First name.";
type = lib.types.str;
};
lastName = lib.mkOption {
description = "Last name.";
type = lib.types.str;
};
groups = lib.mkOption {
description = "Groups this user is member of. The group must exist.";
type = lib.types.listOf lib.types.str;
default = [];
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "File containing the user's password.";
};
};
});
};
};
config = lib.mkIf cfg.enable {
services.nginx = {
enable = true;
virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
locations."/" = {
extraConfig = ''
proxy_set_header Host $host;
'' + (if isNull cfg.restrictAccessIPRange then "" else ''
allow ${cfg.restrictAccessIPRange};
deny all;
'');
proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.services.lldap.settings.http_port}/";
};
};
};
users.users.lldap = {
name = "lldap";
group = "lldap";
isSystemUser = true;
};
users.groups.lldap = {
members = [ "backup" ];
};
services.lldap = {
enable = true;
environment = {
LLDAP_JWT_SECRET_FILE = toString cfg.jwtSecretFile;
LLDAP_LDAP_USER_PASS_FILE = toString cfg.ldapUserPasswordFile;
RUST_LOG = lib.mkIf cfg.debug "debug";
};
settings = {
http_url = "https://${fqdn}";
http_host = "127.0.0.1";
http_port = cfg.webUIListenPort;
ldap_host = "127.0.0.1";
ldap_port = cfg.ldapPort;
ldap_base_dn = cfg.dcdomain;
verbose = cfg.debug;
};
};
environment.systemPackages = [
lldap-cli-auth
];
# $ lldap-cli schema attribute user list
#
# Name Type Is list Is visible Is editable
# ---- ---- ------- ---------- -----------
# avatar JpegPhoto false true true
# creation_date DateTime false true false
# display_name String false true true
# first_name String false true true
# last_name String false true true
# mail String false true true
# user_id String false true false
# uuid String false true false
# $ lldap-cli schema attribute group list
#
# Name Type Is list Is visible Is editable
# ---- ---- ------- ---------- -----------
# creation_date DateTime false true false
# display_name String false true true
# group_id Integer false true false
# uuid String false true false
systemd.services.lldap.postStart =
let
configFile = (pkgs.formats.toml {}).generate "lldap_config.toml" config.services.lldap.settings;
login = [''
set -euo pipefail
sleep 3
export LLDAP_USERNAME=admin
export LLDAP_PASSWORD=$(cat ${cfg.ldapUserPasswordFile})
export LLDAP_HTTPURL=http://${config.services.lldap.settings.http_host}:${toString config.services.lldap.settings.http_port}
eval $(${pkgs.lldap-cli}/bin/lldap-cli login)
set -x
''];
deleteGroups = [''
allUids=(${lib.concatStringsSep " " (
(lib.mapAttrsToList (uid: g: uid) cfg.groups)
++ [ "lldap_admin" "lldap_password_manager" "lldap_strict_readonly" ])
})
echo All managed groups are: $allUids
echo Other groups will be deleted
for uid in $(${pkgs.lldap-cli}/bin/lldap-cli group list); do
if [[ ! " ''${allUids[*]} " =~ [[:space:]]''${uid}[[:space:]] ]]; then
${pkgs.lldap-cli}/bin/lldap-cli group del $uid
fi
done
''];
createGroups = lib.mapAttrsToList (uid: g: ''
${pkgs.lldap-cli}/bin/lldap-cli group add ${uid}
'') cfg.groups;
deleteUsers = [''
allUids=(${lib.concatStringsSep " " (
(lib.mapAttrsToList (uid: u: uid) cfg.users)
++ [ "admin" ])
})
for uid in $(${pkgs.lldap-cli}/bin/lldap-cli user list uid); do
if [[ ! " ''${allUids[*]} " =~ [[:space:]]''${uid}[[:space:]] ]]; then
${pkgs.lldap-cli}/bin/lldap-cli user del $uid
fi
done
''];
createUsers = lib.mapAttrsToList (uid: u: ''
${pkgs.lldap-cli}/bin/lldap-cli user add ${uid} "${u.email}"
${pkgs.lldap-cli}/bin/lldap-cli user update set ${uid} password "$(cat ${u.passwordFile})"
${pkgs.lldap-cli}/bin/lldap-cli user update set ${uid} mail "${u.email}"
${pkgs.lldap-cli}/bin/lldap-cli user update set ${uid} display_name "${u.displayName}"
# ${pkgs.lldap-cli}/bin/lldap-cli user update set ${uid} first_name "${u.firstName}"
# ${pkgs.lldap-cli}/bin/lldap-cli user update set ${uid} last_name "${u.lastName}"
'') cfg.users;
addToGroups = lib.mapAttrsToList (uid: u: lib.concatMapStringsSep "\n" (g: ''
${pkgs.lldap-cli}/bin/lldap-cli user group add \
${uid} \
${g}
'') u.groups) cfg.users;
in
lib.concatStringsSep "\n\n" (
login
++ deleteGroups
++ createGroups
++ deleteUsers
++ createUsers
++ addToGroups
);
shb.backup.instances.lldap = {
sourceDirectories = [
"/var/lib/lldap"
];
};
};
}

View file

@ -1,467 +0,0 @@
{
config,
pkgs,
lib,
shb,
...
}:
let
cfg = config.shb.lldap;
fqdn = "${cfg.subdomain}.${cfg.domain}";
inherit (lib) mkOption types;
ensureFormat = pkgs.formats.json { };
ensureFieldsOptions = name: {
name = mkOption {
type = types.str;
description = "Name of the field.";
default = name;
};
attributeType = mkOption {
type = types.enum [
"STRING"
"INTEGER"
"JPEG"
"DATE_TIME"
];
description = "Attribute type.";
};
isEditable = mkOption {
type = types.bool;
description = "Is field editable.";
default = true;
};
isList = mkOption {
type = types.bool;
description = "Is field a list.";
default = false;
};
isVisible = mkOption {
type = types.bool;
description = "Is field visible in UI.";
default = true;
};
};
in
{
imports = [
../../lib/module.nix
./mitmdump.nix
(lib.mkRenamedOptionModule [ "shb" "ldap" ] [ "shb" "lldap" ])
];
options.shb.lldap = {
enable = lib.mkEnableOption "the LDAP service";
dcdomain = lib.mkOption {
type = lib.types.str;
description = "dc domain to serve.";
example = "dc=mydomain,dc=com";
};
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which the LDAP service will be served.";
example = "grafana";
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain under which the LDAP service will be served.";
example = "mydomain.com";
};
ldapPort = lib.mkOption {
type = lib.types.port;
description = "Port on which the server listens for the LDAP protocol.";
default = 3890;
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr shb.contracts.ssl.certs;
default = null;
};
webUIListenPort = lib.mkOption {
type = lib.types.port;
description = "Port on which the web UI is exposed.";
default = 17170;
};
ldapUserPassword = lib.mkOption {
description = "LDAP admin user secret. Must be >= 8 characters.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
};
jwtSecret = lib.mkOption {
description = "JWT secret.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
};
restrictAccessIPRange = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Set a local network range to restrict access to the UI to only those IPs.";
example = "192.168.1.1/24";
default = null;
};
debug = lib.mkOption {
description = "Enable debug logging.";
type = lib.types.bool;
default = false;
};
mount = lib.mkOption {
type = shb.contracts.mount;
description = ''
Mount configuration. This is an output option.
Use it to initialize a block implementing the "mount" contract.
For example, with a zfs dataset:
```
shb.zfs.datasets."ldap" = {
poolName = "root";
} // config.shb.lldap.mount;
```
'';
readOnly = true;
default = {
path = "/var/lib/lldap";
};
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
type = lib.types.submodule {
options = shb.contracts.backup.mkRequester {
# TODO: is there a workaround that avoid needing to use root?
# root because otherwise we cannot access the private StateDiretory
user = "root";
# /private because the systemd service uses DynamicUser=true
sourceDirectories = [
"/var/lib/private/lldap"
];
};
};
};
ensureUsers = mkOption {
description = ''
Create the users defined here on service startup.
If `enforceUsers` option is `true`, the groups
users belong to must be present in the `ensureGroups` option.
Non-default options must be added to the `ensureGroupFields` option.
'';
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
freeformType = ensureFormat.type;
options = {
id = mkOption {
type = types.str;
description = "Username.";
default = name;
};
email = mkOption {
type = types.str;
description = "Email.";
};
password = mkOption {
description = "Password.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
};
displayName = mkOption {
type = types.nullOr types.str;
default = null;
description = "Display name.";
};
firstName = mkOption {
type = types.nullOr types.str;
default = null;
description = "First name.";
};
lastName = mkOption {
type = types.nullOr types.str;
default = null;
description = "Last name.";
};
avatar_file = mkOption {
type = types.nullOr types.str;
default = null;
description = "Avatar file. Must be a valid path to jpeg file (ignored if avatar_url specified)";
};
avatar_url = mkOption {
type = types.nullOr types.str;
default = null;
description = "Avatar url. must be a valid URL to jpeg file (ignored if gravatar_avatar specified)";
};
gravatar_avatar = mkOption {
type = types.nullOr types.str;
default = null;
description = "Get avatar from Gravatar using the email.";
};
weser_avatar = mkOption {
type = types.nullOr types.str;
default = null;
description = "Convert avatar retrieved by gravatar or the URL.";
};
groups = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Groups the user would be a member of (all the groups must be specified in group config files).";
};
};
}
)
);
};
ensureGroups = mkOption {
description = ''
Create the groups defined here on service startup.
Non-default options must be added to the `ensureGroupFields` option.
'';
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
freeformType = ensureFormat.type;
options = {
name = mkOption {
type = types.str;
description = "Name of the group.";
default = name;
};
};
}
)
);
};
ensureUserFields = mkOption {
description = "Extra fields for users";
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = ensureFieldsOptions name;
}
)
);
};
ensureGroupFields = mkOption {
description = "Extra fields for groups";
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = ensureFieldsOptions name;
}
)
);
};
enforceUsers = mkOption {
description = "Delete users not set declaratively.";
type = types.bool;
default = false;
};
enforceUserMemberships = mkOption {
description = "Remove users from groups not set declaratively.";
type = types.bool;
default = false;
};
enforceGroups = mkOption {
description = "Remove groups not set declaratively.";
type = types.bool;
default = false;
};
dashboard = lib.mkOption {
description = ''
Dashboard contract consumer
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.dashboard.mkRequester {
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
externalUrlText = "https://\${config.shb.lldap.subdomain}.\${config.shb.lldap.domain}";
internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}";
};
};
};
};
config = lib.mkIf cfg.enable {
services.nginx = {
enable = true;
virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
locations."/" = {
extraConfig = ''
proxy_set_header Host $host;
''
+ (
if isNull cfg.restrictAccessIPRange then
""
else
''
allow ${cfg.restrictAccessIPRange};
deny all;
''
);
proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.shb.lldap.webUIListenPort}/";
};
};
};
users.users.lldap = {
name = "lldap";
group = "lldap";
isSystemUser = true;
};
users.groups.lldap = { };
services.lldap = {
enable = true;
inherit (cfg) enforceUsers enforceUserMemberships enforceGroups;
environment = {
RUST_LOG = lib.mkIf cfg.debug "debug";
};
settings = {
http_url = "https://${fqdn}";
http_host = "127.0.0.1";
http_port = if !cfg.debug then cfg.webUIListenPort else cfg.webUIListenPort + 1;
ldap_host = "127.0.0.1";
ldap_port = cfg.ldapPort; # Would be great to be able to inspect this but it requires tcpdump instead of mitmproxy.
ldap_base_dn = cfg.dcdomain;
ldap_user_pass_file = toString cfg.ldapUserPassword.result.path;
force_ldap_user_pass_reset = "always";
jwt_secret_file = toString cfg.jwtSecret.result.path;
verbose = cfg.debug;
};
inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
ensureUsers = lib.mapAttrs (
n: v:
(lib.removeAttrs v [ "password" ])
// {
"password_file" = toString v.password.result.path;
}
) cfg.ensureUsers;
};
# Harden lldap following https://github.com/NixOS/nixpkgs/pull/487933
systemd.services.lldap.serviceConfig = {
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
SystemCallArchitectures = "native";
CapabilityBoundingSet = "";
LockPersonality = true;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
ProcSubset = "pid";
MemoryDenyWriteExecute = true;
};
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
listenPort = config.shb.lldap.webUIListenPort;
upstreamPort = config.shb.lldap.webUIListenPort + 1;
after = [ "lldap.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set"
"verbose_pattern=/api"
];
};
};
}

View file

@ -1,212 +0,0 @@
# LLDAP Block {#blocks-lldap}
Defined in [`/modules/blocks/lldap.nix`](@REPO@/modules/blocks/lldap.nix).
This block sets up an [LLDAP][] service for user and group management
across services.
[LLDAP]: https://github.com/lldap/lldap
## Features {#blocks-lldap-features}
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-lldap-usage-applicationdashboard)
## Usage {#blocks-lldap-usage}
### Initial Configuration {#blocks-lldap-usage-configuration}
```nix
shb.lldap = {
enable = true;
subdomain = "ldap";
domain = "example.com";
dcdomain = "dc=example,dc=com";
ldapPort = 3890;
webUIListenPort = 17170;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result;
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result;
};
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
```
This assumes secrets are setup with SOPS
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
### SSL {#blocks-lldap-usage-ssl}
Using SSL is an important security practice, like always.
Using the [SSL block][], the configuration to add to the one above is:
[SSL block]: blocks-ssl.html
```nix
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
"${config.shb.lldap.subdomain}.${config.shb.lldap.domain}"
];
shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain};
```
### Restrict Access By IP {#blocks-lldap-usage-restrict-access-by-ip}
For added security, you can restrict access to the LLDAP UI
by adding the following line:
```nix
shb.lldap.restrictAccessIPRange = "192.168.50.0/24";
```
### Application Dashboard {#blocks-lldap-usage-applicationdashboard}
Integration with the [dashboard contract](contracts-dashboard.html) is provided
by the [dashboard option](#blocks-lldap-options-shb.lldap.dashboard).
For example using the [Homepage](services-homepage.html) service:
```nix
{
shb.homepage.servicesGroups.Admin.services.LLDAP = {
sortOrder = 2;
dashboard.request = config.shb.lldap.dashboard.request;
};
}
```
## Manage Groups {#blocks-lldap-manage-groups}
The following snippet will create group named "family" if it does not exist yet.
Also, all other groups will be deleted and only the "family" group will remain.
Note that the `lldap_admin`, `lldap_password_manager` and `lldap_strict_readonly` groups, which are internal to LLDAP, will always exist.
If you want existing groups not declared in the `shb.lldap.ensureGroups` to be deleted,
set [`shb.lldap.enforceGroups`](#blocks-lldap-options-shb.lldap.enforceGroups) to `true`.
```nix
{
shb.lldap.ensureGroups = {
family = {};
};
}
```
Changing the configuration to the following will add a new group "friends":
```nix
{
shb.lldap.ensureGroups = {
family = {};
friends = {};
};
}
```
Switching back the configuration to the previous one will delete the group "friends":
```nix
{
shb.lldap.ensureGroups = {
family = {};
};
}
```
Custom fields can be added to groups as long as they are added to the `ensureGroupFields` field:
```nix
shb.lldap = {
ensureGroupFields = {
mygroupattribute = {
attributeType = "STRING";
};
};
ensureGroups = {
family = {
mygroupattribute = "Managed by NixOS";
};
};
};
```
## Manage Users {#blocks-lldap-manage-users}
The following snippet creates a user and makes it a member of the "family" group.
Note the following behavior:
- New users will be created following the `shb.lldap.ensureUsers` option.
- Existing users will be updated, their password included, if they are mentioned in the `shb.lldap.ensureUsers` option.
- Existing users not declared in the `shb.lldap.ensureUsers` will be left as-is.
- User memberships to groups not declared in their respective `shb.lldap.ensureUsers.<name>.groups`.
If you want existing users not declared in the `shb.lldap.ensureUsers` to be deleted,
set [`shb.lldap.enforceUsers`](#blocks-lldap-options-shb.lldap.enforceUsers) to `true`.
If you want memberships to groups not declared in the respective
`shb.lldap.ensureUsers.<name>.groups` option to be deleted,
set [`shb.lldap.enforceUserMemberships`](#blocks-lldap-options-shb.lldap.enforceUserMemberships) `true`.
```nix
{
shb.lldap.ensureUsers = {
dad = {
email = "dad@example.com";
displayName = "Dad";
firstName = "First Name";
lastName = "Last Name";
groups = [ "family" ];
password.result = config.shb.sops.secret."dad".result;
};
};
shb.sops.secret."dad".request =
config.shb.lldap.ensureUsers.dad.password.request;
}
```
The password field assumes usage of the [sops block][] to provide secrets
although any blocks providing the [secrets contract][] works too.
[sops block]: blocks-sops.html
[secrets contract]: contracts-secrets.html
The user is still editable through the UI.
That being said, any change will be overwritten next time the configuration is applied.
## Troubleshooting {#blocks-lldap-troubleshooting}
To increase logging verbosity and see the trace of the GraphQL queries, add:
```nix
shb.lldap.debug = true;
```
Note that verbosity is truly verbose here
so you will want to revert this at some point.
To see the logs, then run `journalctl -u lldap.service`.
Setting the `debug` option to `true` will also
add an [shb.mitmdump][] instance in front of the LLDAP [web UI port](#blocks-lldap-options-shb.lldap.webUIListenPort)
which prints all requests and responses headers and body
to the systemd service `mitmdump-lldap.service`. Note the you won't
see the query done using something like `ldapsearch` since those
go through the [`LDAP` port](#blocks-lldap-options-shb.lldap.ldapPort).
[shb.mitmdump]: ./blocks-mitmdump.html
## Tests {#blocks-lldap-tests}
Specific integration tests are defined in [`/test/blocks/lldap.nix`](@REPO@/test/blocks/lldap.nix).
## Options Reference {#blocks-lldap-options}
```{=include=} options
id-prefix: blocks-lldap-options-
list-id: selfhostblocks-block-lldap-options
source: @OPTIONS_JSON@
```

View file

@ -1,307 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mapAttrs'
mkOption
nameValuePair
types
;
inherit (types)
attrsOf
listOf
port
submodule
str
;
cfg = config.shb.mitmdump;
mitmdumpScript =
pkgs.writers.writePython3Bin "mitmdump"
{
libraries =
let
p = pkgs.python3Packages;
in
[
p.systemd-python
p.mitmproxy
];
flakeIgnore = [ "E501" ];
}
''
from systemd.daemon import notify
import argparse
import logging
import os
import subprocess
import socket
import sys
import time
logging.basicConfig(level=logging.INFO, format='%(message)s')
def wait_for_port(host, port, timeout):
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=0.5):
return True
except Exception:
time.sleep(0.1)
return False
def flatten(xss):
return [x for xs in xss for x in xs]
parser = argparse.ArgumentParser()
parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on")
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost")
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
parser.add_argument("--timeout", required=False, type=int, default=10, help="Timeout to wait for port availability")
args, rest = parser.parse_known_args()
MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN")
if MITMDUMP_BIN is None:
raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary")
logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.")
wait_for_port(args.upstream_host, args.upstream_port, timeout=args.timeout)
logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.")
proc = subprocess.Popen(
[
MITMDUMP_BIN,
"--listen-host", args.listen_host,
"-p", args.listen_port,
"--mode", f"reverse:{args.upstream_host}:{args.upstream_port}",
] + rest,
stdout=sys.stdout,
stderr=sys.stderr,
)
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
if wait_for_port("127.0.0.1", args.listen_port, timeout=args.timeout):
logging.info(f"Mitmdump is started on port {args.listen_port}.")
notify("READY=1")
else:
print(f"Mitmdump instance did not start before the timeout of {args.timeout} seconds, consider increasing the timeout")
proc.terminate()
exit(1)
proc.wait()
'';
logger = toString (
pkgs.writers.writeText "loggerAddon.py" ''
import logging
from collections.abc import Sequence
from mitmproxy import ctx, http
import re
logger = logging.getLogger(__name__)
class RegexLogger:
def __init__(self):
self.verbose_patterns = None
def load(self, loader):
loader.add_option(
name="verbose_pattern",
typespec=Sequence[str],
default=[],
help="Regex patterns for verbose logging",
)
def response(self, flow: http.HTTPFlow):
if self.verbose_patterns is None:
self.verbose_patterns = [re.compile(p) for p in ctx.options.verbose_pattern]
matched = any(p.search(flow.request.path) for p in self.verbose_patterns)
if matched:
logger.info(format_flow(flow))
def format_flow(flow: http.HTTPFlow) -> str:
return (
"\n"
"RequestHeaders:\n"
f" {format_headers(flow.request.headers.items())}\n"
f"RequestBody: {flow.request.get_text()}\n"
f"Status: {flow.response.data.status_code}\n"
"ResponseHeaders:\n"
f" {format_headers(flow.response.headers.items())}\n"
f"ResponseBody: {flow.response.get_text()}\n"
)
def format_headers(headers) -> str:
return "\n ".join(k + ": " + v for k, v in headers)
addons = [RegexLogger()]
''
);
in
{
options.shb.mitmdump = {
addons = mkOption {
type = attrsOf str;
default = [ ];
description = ''
Addons available to the be added to the mitmdump instance.
To enabled them, add them to the `enabledAddons` option.
'';
};
instances = mkOption {
default = { };
description = "Mitmdump instance.";
type = attrsOf (
submodule (
{ name, ... }:
{
options = {
package = lib.mkPackageOption pkgs "mitmproxy" { };
serviceName = mkOption {
type = str;
description = ''
Name of the mitmdump system service.
'';
default = "mitmdump-${name}.service";
readOnly = true;
};
listenHost = mkOption {
type = str;
default = "127.0.0.1";
description = ''
Host the mitmdump instance will connect on.
'';
};
listenPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will listen on.
The upstream port from the client's perspective.
'';
};
upstreamHost = mkOption {
type = str;
default = "http://127.0.0.1";
description = ''
Host the mitmdump instance will connect to.
If only an IP or domain is provided,
mitmdump will default to connect using HTTPS.
If this is not wanted, prefix the IP or domain with the 'http://' protocol.
'';
};
upstreamPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will connect to.
The port the server is listening on.
'';
};
timeout = mkOption {
type = types.int;
default = 30;
description = ''
Time to wait for upstream to start and mitmdump to start.
'';
};
after = mkOption {
type = listOf str;
default = [ ];
description = ''
Systemd services that must be started before this mitmdump proxy instance.
You are guaranteed the mitmdump is listening on the `listenPort`
when its systemd service has started.
'';
};
enabledAddons = mkOption {
type = listOf str;
default = [ ];
description = ''
Addons to enable on this mitmdump instance.
'';
example = lib.literalExpression "[ config.shb.mitmdump.addons.logger ]";
};
extraArgs = mkOption {
type = listOf str;
default = [ ];
description = ''
Extra arguments to pass to the mitmdump instance.
See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options.
'';
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
};
};
}
)
);
};
};
config = {
systemd.services = mapAttrs' (
name: cfg':
nameValuePair "mitmdump-${name}" {
environment = {
"HOME" = "/var/lib/private/mitmdump-${name}";
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
};
serviceConfig = {
Type = "notify";
Restart = "on-failure";
StandardOutput = "journal";
StandardError = "journal";
DynamicUser = true;
WorkingDirectory = "/var/lib/mitmdump-${name}";
StateDirectory = "mitmdump-${name}";
ExecStart =
let
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
in
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} --timeout ${toString cfg'.timeout} ${addons} ${extraArgs}";
};
requires = cfg'.after;
after = cfg'.after;
wantedBy = [ "multi-user.target" ];
}
) cfg.instances;
shb.mitmdump.addons = {
inherit logger;
};
};
}

View file

@ -1,178 +0,0 @@
# Mitmdump Block {#blocks-mitmdump}
Defined in [`/modules/blocks/mitmdump.nix`](@REPO@/modules/blocks/mitmdump.nix).
This block sets up an [Mitmdump][] service in [reverse proxy][] mode.
In other words, you can put this block between a client and a server to inspect all the network traffic.
[Mitmdump]: https://plattner.me/mp-docs/#mitmdump
[reverse proxy]: https://plattner.me/mp-docs/concepts-modes/#reverse-proxy
Multiple instances of mitmdump all listening on different ports
and proxying to different upstream servers can be created.
The systemd service is made so it is started only when the mitmdump instance
has started listening on the expected port.
Also, addons can be enabled with the `enabledAddons` option.
## Usage {#blocks-mitmdump-usage}
Put mitmdump in front of a HTTP server listening on port 8000 on the same machine:
```nix
shb.mitmdump.instances."my-instance" = {
listenPort = 8001;
upstreamHost = "http://127.0.0.1";
upstreamPort = 8000;
after = [ "server.service" ];
};
```
`upstreamHost` has its default value here and can be left out.
Put mitmdump in front of a HTTP server listening on port 8000 on another machine:
```nix
shb.mitmdump.instances."my-instance" = {
listenPort = 8001;
upstreamHost = "http://otherhost";
upstreamPort = 8000;
after = [ "server.service" ];
};
```
### Handle Upstream TLS {#blocks-mitmdump-usage-https}
Replace `http` with `https` if the server expects an HTTPS connection.
### Accept Connections from Anywhere {#blocks-mitmdump-usage-anywhere}
By default, `mitmdump` is configured to listen only for connections from localhost.
Add `listenHost=0.0.0.0` to make `mitmdump` accept connections from anywhere.
### Extra Logging {#blocks-mitmdump-usage-logging}
To print request and response bodies and more, increase the logging with:
```nix
extraArgs = [
"--set" "flow_detail=3"
"--set" "content_view_lines_cutoff=2000"
];
```
The default `flow_details` is 1. See the [manual][] for more explanations on the option.
[manual]: (https://docs.mitmproxy.org/stable/concepts/options/#flow_detail)
This will change the verbosity for all requests and responses.
If you need more fine grained logging, configure instead the [Logger Addon][].
[Logger Addon]: #blocks-mitmdump-addons-logger
## Addons {#blocks-mitmdump-addons}
All provided addons can be found under the `shb.mitmproxy.addons` option.
To enable one for an instance, add it to the `enabledAddons` option. For example:
```nix
shb.mitmdump.instances."my-instance" = {
enabledAddons = [ config.shb.mitmdump.addons.logger ]
}
```
### Fine Grained Logger {#blocks-mitmdump-addons-logger}
The Fine Grained Logger addon is found under `shb.mitmproxy.addons.logger`.
Enabling this addon will add the `mitmdump` option `verbose_pattern` which takes a regex and if it matches,
prints the request and response headers and body.
If it does not match, it will just print the response status.
For example, with the `extraArgs`:
```nix
extraArgs = [
"--set" "verbose_pattern=/verbose"
];
```
A `GET` request to `/notverbose` will print something similar to:
```
mitmdump[972]: 127.0.0.1:53586: GET http://127.0.0.1:8000/notverbose HTTP/1.1
mitmdump[972]: << HTTP/1.0 200 OK 16b
```
While a `GET` request to `/verbose` will print something similar to:
```
mitmdump[972]: [22:42:58.840]
mitmdump[972]: RequestHeaders:
mitmdump[972]: Host: 127.0.0.1:8000
mitmdump[972]: User-Agent: curl/8.14.1
mitmdump[972]: Accept: */*
mitmdump[972]: RequestBody:
mitmdump[972]: Status: 200
mitmdump[972]: ResponseHeaders:
mitmdump[972]: Server: BaseHTTP/0.6 Python/3.13.4
mitmdump[972]: Date: Sun, 03 Aug 2025 22:42:58 GMT
mitmdump[972]: Content-Type: text/plain
mitmdump[972]: Content-Length: 13
mitmdump[972]: ResponseBody: test2/verbose
mitmdump[972]: 127.0.0.1:53602: GET http://127.0.0.1:8000/verbose HTTP/1.1
mitmdump[972]: << HTTP/1.0 200 OK 13b
```
## Example {#blocks-mitmdump-example}
Let's assume a server is listening on port 8000
which responds a plain text response `test1`
and its related systemd service is named `test1.service`.
Sorry, creative naming is not my forte.
Let's put an mitmdump instance in front of it, like so:
```nix
shb.mitmdump.instances."test1" = {
listenPort = 8001;
upstreamPort = 8000;
after = [ "test1.service" ];
extraArgs = [
"--set" "flow_detail=3"
"--set" "content_view_lines_cutoff=2000"
];
};
```
This creates an `mitmdump-test1.service` systemd service.
We can then use `journalctl -u mitmdump-test1.service` to see the output.
If we make a `curl` request to it: `curl -v http://127.0.0.1:8001`,
we will get the following output:
```
mitmdump-test1[971]: 127.0.0.1:40878: GET http://127.0.0.1:8000/ HTTP/1.1
mitmdump-test1[971]: Host: 127.0.0.1:8000
mitmdump-test1[971]: User-Agent: curl/8.14.1
mitmdump-test1[971]: Accept: */*
mitmdump-test1[971]: << HTTP/1.0 200 OK 5b
mitmdump-test1[971]: Server: BaseHTTP/0.6 Python/3.13.4
mitmdump-test1[971]: Date: Thu, 31 Jul 2025 20:55:16 GMT
mitmdump-test1[971]: Content-Type: text/plain
mitmdump-test1[971]: Content-Length: 5
mitmdump-test1[971]: test1
```
## Tests {#blocks-mitmdump-tests}
Specific integration tests are defined in [`/test/blocks/mitmdump.nix`](@REPO@/test/blocks/mitmdump.nix).
## Options Reference {#blocks-mitmdump-options}
```{=include=} options
id-prefix: blocks-mitmdump-options-
list-id: selfhostblocks-block-mitmdump-options
source: @OPTIONS_JSON@
```

View file

@ -1,56 +1,31 @@
{ { config, options, pkgs, lib, ... }:
config,
pkgs,
lib,
shb,
...
}:
let let
cfg = config.shb.monitoring; cfg = config.shb.monitoring;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
commonLabels = {
hostname = config.networking.hostName;
domain = cfg.domain;
};
roleClaim = "grafana_groups";
oauthScopes = [
"openid"
"email"
"profile"
"groups"
"${roleClaim}"
];
in in
{ {
imports = [
../../lib/module.nix
../blocks/authelia.nix
../blocks/lldap.nix
../blocks/nginx.nix
];
options.shb.monitoring = { options.shb.monitoring = {
enable = lib.mkEnableOption "selfhostblocks.monitoring"; enable = lib.mkEnableOption "selfhostblocks.monitoring";
subdomain = lib.mkOption { subdomain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Subdomain under which Grafana will be served."; description = "Subdomain under which home-assistant will be served.";
example = "grafana"; example = "grafana";
}; };
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "domain under which Grafana will be served."; description = "domain under which home-assistant will be served.";
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption { ssl = lib.mkOption {
description = "Path to SSL files"; description = "Path to SSL files";
type = lib.types.nullOr shb.contracts.ssl.certs; type = lib.types.nullOr contracts.ssl.certs;
default = null; default = null;
}; };
@ -73,10 +48,7 @@ in
}; };
lokiMajorVersion = lib.mkOption { lokiMajorVersion = lib.mkOption {
type = lib.types.enum [ type = lib.types.enum [ 2 3 ];
2
3
];
description = '' description = ''
Switching from version 2 to 3 requires manual intervention Switching from version 2 to 3 requires manual intervention
https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user
@ -98,10 +70,10 @@ in
default = 1; default = 1;
}; };
dashboards = lib.mkOption { provisionDashboards = lib.mkOption {
type = lib.types.listOf lib.types.path; type = lib.types.bool;
description = "Dashboards to provision under 'Self Host Blocks' folder."; description = "Provision Self Host Blocks dashboards under 'Self Host Blocks' folder.";
default = [ ]; default = true;
}; };
contactPoints = lib.mkOption { contactPoints = lib.mkOption {
@ -110,64 +82,20 @@ in
default = []; default = [];
}; };
scrutiny = { adminPasswordFile = lib.mkOption {
enable = lib.mkEnableOption "scrutiny service" // { type = lib.types.path;
default = true; description = "File containing the initial admin password.";
};
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
If a string, this will be the subdomain under which the scrutiny web interface will be servced.
If null, the web interface will not be served and only the prometheus metrics will be accessible.
'';
default = "scrutiny";
};
dashboard = lib.mkOption {
description = ''
Dashboard contract consumer
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.dashboard.mkRequester {
externalUrl = "https://${cfg.scrutiny.subdomain}.${cfg.domain}";
externalUrlText = "https://\${config.shb.monitoring.scrutiny.subdomain}.\${config.shb.monitoring.domain}";
internalUrl = "http://127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}";
internalUrlText = "https://127.0.0.1.\${config.services.scrutiny.settings.web.listen.port}";
};
};
};
}; };
adminPassword = lib.mkOption { secretKeyFile = lib.mkOption {
description = "Initial admin password."; type = lib.types.path;
type = lib.types.submodule { description = "File containing the secret key used for signing.";
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = "grafana";
group = "grafana";
restartUnits = [ "grafana.service" ];
};
};
};
secretKey = lib.mkOption {
description = "Secret key used for signing.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = "grafana";
group = "grafana";
restartUnits = [ "grafana.service" ];
};
};
}; };
smtp = lib.mkOption { smtp = lib.mkOption {
description = "SMTP options."; description = "SMTP options.";
default = null; default = null;
type = lib.types.nullOr ( type = lib.types.nullOr (lib.types.submodule {
lib.types.submodule {
options = { options = {
from_address = lib.mkOption { from_address = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -177,7 +105,7 @@ in
from_name = lib.mkOption { from_name = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "SMTP name from which the emails originate."; description = "SMTP name from which the emails originate.";
default = "Grafana"; default = "Vaultwarden";
}; };
host = lib.mkOption { host = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -197,119 +125,14 @@ in
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
}; };
}; };
} });
);
};
ldap = lib.mkOption {
description = ''
Setup LDAP integration.
'';
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
type = lib.types.str;
description = "Group users must belong to to be able to login to Grafana.";
default = "monitoring_user";
};
adminGroup = lib.mkOption {
type = lib.types.str;
description = "Group users must belong to to be admins in Grafana.";
default = "monitoring_admin";
};
};
}; };
}; };
sso = lib.mkOption { config = lib.mkIf cfg.enable {
description = ''
Setup SSO integration.
'';
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
authEndpoint = lib.mkOption {
type = lib.types.str;
default = null;
description = "Endpoint to the SSO provider.";
example = "https://authelia.example.com";
};
clientID = lib.mkOption {
type = lib.types.str;
description = "Client ID for the OIDC endpoint.";
default = "grafana";
};
authorization_policy = lib.mkOption {
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
sharedSecret = lib.mkOption {
description = "OIDC shared secret for Grafana.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
owner = "grafana";
restartUnits = [
"grafana.service"
];
};
};
};
sharedSecretForAuthelia = lib.mkOption {
description = "OIDC shared secret for Authelia. Must be the same as `sharedSecret`";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
ownerText = "config.shb.authelia.autheliaUser";
owner = config.shb.authelia.autheliaUser;
};
};
};
};
};
};
dashboard = lib.mkOption {
description = ''
Dashboard contract consumer
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.dashboard.mkRequester {
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
externalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
internalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
};
};
};
impermanence = lib.mkOption {
description = ''
Paths to save when using impermanence setup.
'';
type = lib.types.attrsOf lib.types.str;
default = {
fluent-bit = "/var/fluent-bit";
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
assertions = [ assertions = [
{ {
assertion = builtins.length cfg.contactPoints > 0; assertion = (!(isNull cfg.smtp)) -> builtins.length cfg.contactPoints > 0;
message = "Must have at least one contact point for alerting"; message = "Must have at least one contact point for alerting";
} }
]; ];
@ -336,9 +159,9 @@ in
}; };
security = { security = {
secret_key = "$__file{${cfg.secretKey.result.path}}"; secret_key = "$__file{${cfg.secretKeyFile}}";
disable_initial_admin_creation = false; # Enable when LDAP support is configured. disable_initial_admin_creation = false; # Enable when LDAP support is configured.
admin_password = "$__file{${cfg.adminPassword.result.path}}"; # Remove when LDAP support is configured. admin_password = "$__file{${cfg.adminPasswordFile}}"; # Remove when LDAP support is configured.
}; };
server = { server = {
@ -358,29 +181,16 @@ in
}; };
}; };
}; };
})
(lib.mkIf cfg.enable {
shb.monitoring.dashboards = [
./monitoring/dashboards/Errors.json
./monitoring/dashboards/Performance.json
./monitoring/dashboards/Scraping_Jobs.json
];
services.grafana.provision = { services.grafana.provision = {
dashboards.settings = lib.mkIf (cfg.dashboards != [ ]) { dashboards.settings = lib.mkIf cfg.provisionDashboards {
apiVersion = 1; apiVersion = 1;
providers = [ providers = [{
{
folder = "Self Host Blocks"; folder = "Self Host Blocks";
options.path = pkgs.symlinkJoin { options.path = ./monitoring/dashboards;
name = "dashboards";
paths = map (p: pkgs.runCommand "dashboard" { } "mkdir $out; cp ${p} $out") cfg.dashboards;
};
allowUiUpdates = true; allowUiUpdates = true;
disableDeletion = true; disableDeletion = true;
} }];
];
}; };
datasources.settings = { datasources.settings = {
apiVersion = 1; apiVersion = 1;
@ -416,78 +226,59 @@ in
}; };
alerting.contactPoints.settings = { alerting.contactPoints.settings = {
apiVersion = 1; apiVersion = 1;
contactPoints = [ contactPoints = [{
{
inherit (cfg) orgId; inherit (cfg) orgId;
name = "grafana-default-email"; name = "grafana-default-email";
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [ receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [{
{
uid = "sysadmin"; uid = "sysadmin";
type = "email"; type = "email";
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints; settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
} }];
]; }];
}
];
}; };
alerting.policies.settings = { alerting.policies.settings = {
apiVersion = 1; apiVersion = 1;
policies = [ policies = [{
{
inherit (cfg) orgId; inherit (cfg) orgId;
receiver = "grafana-default-email"; receiver = "grafana-default-email";
group_by = [ group_by = [ "grafana_folder" "alertname" ];
"grafana_folder"
"alertname"
];
group_wait = "30s"; group_wait = "30s";
group_interval = "5m"; group_interval = "5m";
repeat_interval = "4h"; repeat_interval = "4h";
} }];
];
# resetPolicies seems to happen after setting the above policies, effectively rolling back # resetPolicies seems to happen after setting the above policies, effectively rolling back
# any updates. # any updates.
}; };
alerting.rules.settings = alerting.rules.settings =
let let
rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json); rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json);
ruleIds = map (r: r.uid) rules;
in in
{ {
apiVersion = 1; apiVersion = 1;
groups = [ groups = [{
{
inherit (cfg) orgId; inherit (cfg) orgId;
name = "SysAdmin"; name = "SysAdmin";
folder = "Self Host Blocks"; folder = "Self Host Blocks";
interval = "10m"; interval = "10m";
inherit rules; inherit rules;
} }];
];
# deleteRules seems to happen after creating the above rules, effectively rolling back # deleteRules seems to happen after creating the above rules, effectively rolling back
# any updates. # any updates.
}; };
}; };
})
(lib.mkIf cfg.enable {
services.prometheus = { services.prometheus = {
enable = true; enable = true;
port = cfg.prometheusPort; port = cfg.prometheusPort;
globalConfig = {
scrape_interval = "15s";
};
}; };
services.loki = { services.loki = {
enable = true; enable = true;
dataDir = "/var/lib/loki"; dataDir = "/var/lib/loki";
package = package = if cfg.lokiMajorVersion == 3 then pkgs.grafana-loki else
if cfg.lokiMajorVersion == 3 then
pkgs.grafana-loki
else
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4 # Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
(pkgs.grafana-loki.overrideAttrs ( (pkgs.grafana-loki.overrideAttrs (finalAttrs: previousAttrs: rec {
finalAttrs: previousAttrs: rec {
version = "2.9.6"; version = "2.9.6";
src = pkgs.fetchFromGitHub { src = pkgs.fetchFromGitHub {
@ -497,19 +288,7 @@ in
hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I="; hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I=";
}; };
subPackages = [ ldflags = let t = "github.com/grafana/loki/pkg/util/build"; in [
"cmd/loki"
"cmd/loki-canary"
"clients/cmd/promtail"
"cmd/logcli"
# Removes "cmd/lokitool"
];
ldflags =
let
t = "github.com/grafana/loki/pkg/util/build";
in
[
"-s" "-s"
"-w" "-w"
"-X ${t}.Version=${version}" "-X ${t}.Version=${version}"
@ -518,8 +297,7 @@ in
"-X ${t}.Branch=unknown" "-X ${t}.Branch=unknown"
"-X ${t}.Revision=unknown" "-X ${t}.Revision=unknown"
]; ];
} }));
));
configuration = { configuration = {
auth_enabled = false; auth_enabled = false;
@ -585,63 +363,40 @@ in
}; };
}; };
# I decided to switch to fluent-bit because it can be tested locally https://docs.fluentbit.io/manual/local-testing/logging-pipeline services.promtail = {
services.fluent-bit = {
enable = true; enable = true;
settings = { configuration = {
service = { server = {
flush = 1; http_listen_port = 9080;
log_level = "info"; grpc_listen_port = 0;
http_server = "true";
http_listen = "127.0.0.1";
http_port = 9080;
grace = 30;
}; };
pipeline = { positions.filename = "/tmp/positions.yaml";
inputs = [
client.url = "http://localhost:${toString config.services.loki.configuration.server.http_listen_port}/api/prom/push";
scrape_configs = [
{ {
name = "systemd"; job_name = "systemd";
journal = {
# The asterisk appends the _SYSTEMD_UNIT to the prefix. json = false;
tag = "systemd.*"; max_age = "12h";
# Read logs from this systemd journal directory.
path = "/var/log/journal"; path = "/var/log/journal";
# matches = "_TRANSPORT=kernel";
# Database file to keep track of the journald cursor. labels = {
db = "/var/fluent-bit/systemd.db";
# Start reading new entries. Skip entries already stored in journald.
read_from_tail = true;
# Max entries to lookback on start.
max_entries = 10000;
}
];
outputs = [
{
name = "loki";
match = "systemd.*";
host = "localhost";
port = config.services.loki.configuration.server.http_listen_port;
labels = lib.concatMapAttrsStringSep ", " (n: v: "${n}=${v}") {
job = "systemd-journal"; job = "systemd-journal";
domain = cfg.domain;
hostname = config.networking.hostName;
}; };
};
label_keys = "$unit"; relabel_configs = [
{
source_labels = [ "__journal__systemd_unit" ];
target_label = "unit";
}
];
} }
]; ];
}; };
}; };
graceLimit = "1m";
};
services.nginx = { services.nginx = {
enable = true; enable = true;
@ -660,67 +415,40 @@ in
}; };
}; };
}; };
})
(lib.mkIf cfg.enable {
services.prometheus.scrapeConfigs = [ services.prometheus.scrapeConfigs = [
{ {
job_name = "node"; job_name = "node";
static_configs = [ static_configs = [{
{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"]; targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"];
labels = commonLabels; }];
}
];
} }
{ {
job_name = "netdata"; job_name = "netdata";
metrics_path = "/api/v1/allmetrics"; metrics_path = "/api/v1/allmetrics";
params.format = [ "prometheus" ]; params.format = [ "prometheus" ];
honor_labels = true; honor_labels = true;
static_configs = [ static_configs = [{
{
targets = [ "127.0.0.1:19999" ]; targets = [ "127.0.0.1:19999" ];
labels = commonLabels; }];
}
];
} }
{ {
job_name = "smartctl"; job_name = "smartctl";
static_configs = [ static_configs = [{
{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"]; targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"];
labels = commonLabels; }];
}
];
} }
{ {
job_name = "prometheus_internal"; job_name = "prometheus_internal";
static_configs = [ static_configs = [{
{
targets = ["127.0.0.1:${toString config.services.prometheus.port}"]; targets = ["127.0.0.1:${toString config.services.prometheus.port}"];
labels = commonLabels; }];
} }
]; ] ++ (lib.lists.optional config.services.nginx.enable {
}
{
job_name = "systemd";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.systemd.port}" ];
labels = commonLabels;
}
];
}
]
++ (lib.lists.optional config.services.nginx.enable {
job_name = "nginx"; job_name = "nginx";
static_configs = [ static_configs = [{
{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"]; targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"];
labels = commonLabels; }];
}
];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) { # }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
# job_name = "redis"; # job_name = "redis";
# static_configs = [ # static_configs = [
@ -735,15 +463,11 @@ in
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"]; # targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
# } # }
# ]; # ];
}) }) ++ (lib.optional config.services.dnsmasq.enable {
++ (lib.optional config.services.dnsmasq.enable {
job_name = "dnsmasq"; job_name = "dnsmasq";
static_configs = [ static_configs = [{
{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"]; targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"];
labels = commonLabels; }];
}
];
}); });
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable { services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
enable = true; enable = true;
@ -754,41 +478,7 @@ in
services.prometheus.exporters.node = { services.prometheus.exporters.node = {
enable = true; enable = true;
# https://github.com/prometheus/node_exporter#collectors # https://github.com/prometheus/node_exporter#collectors
enabledCollectors = [ enabledCollectors = ["ethtool"];
"arp"
"cpu"
"cpufreq"
"diskstats"
"dmi"
"edac"
"entropy"
"filefd"
"filesystem"
"hwmon"
"loadavg"
"meminfo"
"netclass"
"netdev"
"netstat"
"nvme"
"os"
"pressure"
"rapl"
"schedstat"
"stat"
"thermal_zone"
"time"
"uname"
"vmstat"
"zfs"
# Disabled by default
"cgroups"
"drm"
"ethtool"
"logind"
"wifi"
];
port = 9112; port = 9112;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
}; };
@ -818,11 +508,6 @@ in
port = 9211; port = 9211;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
}; };
services.prometheus.exporters.systemd = {
enable = true;
port = 9116;
listenAddress = "127.0.0.1";
};
services.nginx.statusPage = lib.mkDefault config.services.nginx.enable; services.nginx.statusPage = lib.mkDefault config.services.nginx.enable;
services.netdata = { services.netdata = {
enable = true; enable = true;
@ -836,155 +521,5 @@ in
}; };
}; };
}; };
nixpkgs.overlays = [
(final: prev: {
prometheus-systemd-exporter = prev.prometheus-systemd-exporter.overrideAttrs {
src = final.fetchFromGitHub {
owner = "ibizaman";
repo = prev.prometheus-systemd-exporter.pname;
# rev = "v${prev.prometheus-systemd-exporter.version}";
rev = "next_timer";
sha256 = "sha256-jzkh/616tsJbNxFtZ0xbdBQc16TMIYr9QOkPaeQw8xA=";
}; };
vendorHash = "sha256-4hsQ1417jLNOAqGkfCkzrmEtYR4YLLW2j0CiJtPg6GI=";
};
})
];
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = { };
${cfg.ldap.adminGroup} = { };
};
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
# Roles are: None, Viewer, Editor, Admin, GrafanaAdmin
''"${cfg.ldap.adminGroup}" in groups ? "Admin" : ("${cfg.ldap.userGroup}" in groups ? "Editor" : "Invalid")'';
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = { };
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
claims = [ "${roleClaim}" ];
};
services.grafana.settings."auth.generic_oauth" = {
enabled = true;
name = "Authelia";
icon = "signin";
client_id = cfg.sso.clientID;
client_secret = "$__file{${cfg.sso.sharedSecret.result.path}}";
scopes = oauthScopes;
empty_scopes = false;
allow_sign_up = true;
auto_login = true;
auth_url = "${cfg.sso.authEndpoint}/api/oidc/authorization";
token_url = "${cfg.sso.authEndpoint}/api/oidc/token";
# use_refresh_token = true; ? # https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/generic-oauth/#configure-generic-oauth-authentication-client-using-the-grafana-configuration-file
api_url = "${cfg.sso.authEndpoint}/api/oidc/userinfo";
login_attribute_path = "preferred_username";
groups_attribute_path = "groups";
name_attribute_path = "name";
use_pkce = true;
allow_assign_grafana_admin = true;
skip_org_role_sync = false;
role_attribute_path = roleClaim;
role_attribute_strict = true;
};
shb.authelia.oidcClients = [
{
client_id = cfg.sso.clientID;
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
claims_policy = "${roleClaim}";
scopes = oauthScopes;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/login/generic_oauth"
];
require_pkce = true;
pkce_challenge_method = "S256";
response_types = [ "code" ];
token_endpoint_auth_method = "client_secret_basic";
}
];
})
(lib.mkIf (cfg.enable && cfg.scrutiny.enable) {
services.scrutiny = {
enable = true;
openFirewall = false;
# This src includes Prometheus metrics exporter.
package = pkgs.scrutiny.overrideAttrs ({
src = pkgs.fetchFromGitHub {
owner = "ibizaman";
repo = "scrutiny";
rev = "74faf06f77df83f29e7e1806cd88b2fafc0bbb82";
hash = "sha256-r0AVWL+E046xHxitwMPfRNTOpjuOk+W6tB41YgmLTPg=";
};
vendorHash = "sha256-kAlnlWnBMFCdgdak5L5hRquRtyLi5MTmDa/kxwqPs4E=";
});
settings = {
web = {
metrics.enabled = true; # Enables Prometheus exporter
listenHost = "127.0.0.1";
};
};
collector = {
enable = true;
};
};
services.prometheus.scrapeConfigs = [
{
job_name = "scrutiny";
metrics_path = "/api/metrics";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}" ];
labels = commonLabels;
}
];
}
];
shb.monitoring.dashboards = [
./monitoring/dashboards/Health.json
];
shb.nginx.vhosts = lib.mkIf (cfg.scrutiny.subdomain != null) [
(
{
inherit (cfg) domain ssl;
subdomain = cfg.scrutiny.subdomain;
upstream = "http://127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}";
autheliaRules = lib.optionals (cfg.sso.enable) [
{
domain = "${cfg.subdomain}.${cfg.domain}";
policy = cfg.sso.authorization_policy;
subject = [
"group:${cfg.ldap.userGroup}"
"group:${cfg.ldap.adminGroup}"
];
}
];
}
// lib.optionalAttrs cfg.sso.enable {
inherit (cfg.sso) authEndpoint;
}
)
];
})
];
} }

View file

@ -20,6 +20,7 @@
"graphTooltip": 0, "graphTooltip": 0,
"id": 8, "id": 8,
"links": [], "links": [],
"liveNow": false,
"panels": [ "panels": [
{ {
"datasource": { "datasource": {
@ -38,7 +39,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -110,7 +110,7 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -182,7 +182,6 @@
"axisPlacement": "auto", "axisPlacement": "auto",
"axisSoftMin": 0.5, "axisSoftMin": 0.5,
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -255,7 +254,7 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -326,7 +325,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -400,7 +398,7 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -459,10 +457,6 @@
"type": "loki", "type": "loki",
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"fieldConfig": {
"defaults": {},
"overrides": []
},
"gridPos": { "gridPos": {
"h": 8, "h": 8,
"w": 24, "w": 24,
@ -480,7 +474,6 @@
"sortOrder": "Descending", "sortOrder": "Descending",
"wrapLogMessage": false "wrapLogMessage": false
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -519,7 +512,8 @@
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "green" "color": "green",
"value": null
}, },
{ {
"color": "red", "color": "red",
@ -749,23 +743,28 @@
"type": "table" "type": "table"
} }
], ],
"preload": false,
"refresh": "", "refresh": "",
"schemaVersion": 40, "schemaVersion": 38,
"tags": [], "tags": [],
"templating": { "templating": {
"list": [ "list": [
{ {
"allValue": ".+", "allValue": ".+",
"current": { "current": {
"text": "All", "selected": true,
"value": "$__all" "text": [
"All"
],
"value": [
"$__all"
]
}, },
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"definition": "query_result(max by (name) (node_systemd_unit_state))", "definition": "query_result(max by (name) (node_systemd_unit_state))",
"hide": 0,
"includeAll": true, "includeAll": true,
"multi": true, "multi": true,
"name": "service", "name": "service",
@ -777,23 +776,27 @@
}, },
"refresh": 1, "refresh": 1,
"regex": "/name=\"(?<value>.*)\\.service\"/", "regex": "/name=\"(?<value>.*)\\.service\"/",
"skipUrlSync": false,
"sort": 1, "sort": 1,
"type": "query" "type": "query"
}, },
{ {
"current": { "current": {
"text": ".+", "selected": false,
"value": ".+" "text": "",
"value": ""
}, },
"hide": 0,
"name": "server_name", "name": "server_name",
"options": [ "options": [
{ {
"selected": true, "selected": true,
"text": ".+", "text": "",
"value": ".+" "value": ""
} }
], ],
"query": ".+", "query": ".+",
"skipUrlSync": false,
"type": "textbox" "type": "textbox"
} }
] ]
@ -806,6 +809,6 @@
"timezone": "", "timezone": "",
"title": "Errors", "title": "Errors",
"uid": "d66242cf-71e8-417c-8ef7-51b0741545df", "uid": "d66242cf-71e8-417c-8ef7-51b0741545df",
"version": 32, "version": 29,
"weekStart": "" "weekStart": ""
} }

View file

@ -1,872 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "hostname",
"title": "${hostname}",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"footer": {
"reducers": []
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 1
},
"id": 3,
"maxDataPoints": 400,
"options": {
"cellHeight": "sm",
"showHeader": true
},
"pluginVersion": "12.4.0",
"targets": [
{
"disableTextWrap": false,
"editorMode": "builder",
"expr": "node_os_info",
"fullMetaSearch": false,
"includeNullMetadata": true,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
}
],
"title": "OS Versions",
"transformations": [
{
"id": "labelsToFields",
"options": {
"keepLabels": [
"build_id",
"domain",
"hostname",
"id",
"instance",
"job",
"name",
"pretty_name",
"version",
"version_codename",
"version_id"
],
"mode": "columns"
}
},
{
"id": "groupBy",
"options": {
"fields": {
"Time": {
"aggregations": [
"firstNotNull"
],
"operation": "aggregate"
},
"pretty_name": {
"aggregations": [],
"operation": "groupby"
}
}
}
}
],
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 1
},
"id": 1,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "node_hwmon_temp_celsius{hostname=~\"$hostname\"}",
"instant": false,
"legendFormat": "{{chip}} - {{sensor}}",
"range": true,
"refId": "A"
}
],
"title": "Temperature",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisPlacement": "auto",
"fillOpacity": 70,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 9
},
"id": 5,
"interval": "1m",
"maxDataPoints": 200,
"options": {
"colWidth": 1,
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"rowHeight": 0.8,
"showValue": "never",
"tooltip": {
"hideZeros": false,
"mode": "none",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"repeat": "hostname",
"repeatDirection": "h",
"targets": [
{
"editorMode": "code",
"expr": "node_zfs_zpool_state{hostname=~\"$hostname\", state=\"online\"} > 0",
"legendFormat": "{{zpool}} - {{state}}",
"range": true,
"refId": "A"
}
],
"title": "ZFS Pools",
"type": "status-history"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line+area"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "transparent",
"value": 604808
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 13
},
"id": 2,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "ssl_certificate_expiry_seconds",
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
"range": true,
"refId": "A"
}
],
"title": "Certificate Remaining Validity",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "years"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 13
},
"id": 7,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"targets": [
{
"editorMode": "builder",
"expr": "scrutiny_smart_power_on_hours{hostname=~\"$hostname\"} / (24 * 365)",
"legendFormat": "{{device_name}}",
"range": true,
"refId": "A"
}
],
"title": "Operating Years",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-YlRd"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMax": 100,
"axisSoftMin": 0,
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 21
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
"width": 400
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"targets": [
{
"disableTextWrap": false,
"editorMode": "builder",
"expr": "sum by(hostname, domain, mountpoint, device) (node_filesystem_free_bytes{hostname=~\"$hostname\"})",
"fullMetaSearch": false,
"hide": true,
"includeNullMetadata": false,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "builder",
"expr": "sum by(hostname, domain, mountpoint, device) (node_filesystem_size_bytes{hostname=~\"$hostname\"})",
"hide": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "B"
},
{
"datasource": {
"name": "Expression",
"type": "__expr__",
"uid": "__expr__"
},
"expression": "(1 - $A / $B) * 100",
"refId": "Disk Full",
"type": "math"
}
],
"title": "Filesystem Disk Usage",
"transformations": [
{
"id": "joinByField",
"options": {
"byField": "Time",
"mode": "outer"
}
}
],
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 21
},
"id": 9,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0",
"targets": [
{
"editorMode": "builder",
"expr": "scrutiny_smart_power_on_hours{hostname=~\"$hostname\"}",
"legendFormat": "{{device_name}}",
"range": true,
"refId": "A"
}
],
"title": "Operating Years",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"footer": {
"reducers": []
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 29
},
"id": 8,
"options": {
"cellHeight": "sm",
"showHeader": true
},
"pluginVersion": "12.4.0",
"targets": [
{
"editorMode": "builder",
"exemplar": false,
"expr": "scrutiny_device_info{hostname=~\"$hostname\"}",
"format": "table",
"instant": true,
"legendFormat": "{{device_name}}",
"range": false,
"refId": "A"
}
],
"title": "Disk Info",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Value": true,
"__name__": true,
"domain": true,
"hostname": true,
"instance": true,
"job": true,
"wwn": false
},
"includeByName": {},
"indexByName": {},
"renameByName": {}
}
}
],
"type": "table"
}
],
"preload": false,
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": "baryum",
"value": "baryum"
},
"definition": "label_values(up,hostname)",
"name": "hostname",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(up,hostname)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query"
}
]
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Node Health",
"uid": "edhuvl28vpjwge",
"version": 25,
"weekStart": ""
}

View file

@ -20,9 +20,9 @@
"graphTooltip": 0, "graphTooltip": 0,
"id": 6, "id": 6,
"links": [], "links": [],
"liveNow": false,
"panels": [ "panels": [
{ {
"collapsed": false,
"gridPos": { "gridPos": {
"h": 1, "h": 1,
"w": 24, "w": 24,
@ -30,7 +30,6 @@
"y": 0 "y": 0
}, },
"id": 12, "id": 12,
"panels": [],
"title": "Node", "title": "Node",
"type": "row" "type": "row"
}, },
@ -52,7 +51,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -206,7 +204,6 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -214,7 +211,7 @@
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "netdata_system_memory_full_pressure_stall_time_ms_average{hostname=~\"$hostname\"} * -1", "expr": "netdata_system_memory_full_pressure_stall_time_ms_average{instance=~\"$instance\"} * -1",
"hide": false, "hide": false,
"instant": false, "instant": false,
"legendFormat": "full stall time", "legendFormat": "full stall time",
@ -272,7 +269,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -426,7 +422,6 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -434,7 +429,7 @@
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "netdata_system_memory_full_pressure_stall_time_ms_average{hostname=~\"$hostname\"} * -1", "expr": "netdata_system_memory_full_pressure_stall_time_ms_average{instance=~\"$instance\"} * -1",
"hide": false, "hide": false,
"instant": false, "instant": false,
"legendFormat": "full stall time", "legendFormat": "full stall time",
@ -461,7 +456,7 @@
}, },
"disableTextWrap": false, "disableTextWrap": false,
"editorMode": "code", "editorMode": "code",
"expr": "sum by(dimension, service_name) (netdata_systemd_service_memory_usage_MiB_average{hostname=~\"$hostname\", service_name=~\"$service\", dimension=\"swap\"})", "expr": "sum by(dimension, service_name) (netdata_systemd_service_memory_usage_MiB_average{instance=~\"$instance\", service_name=~\"$service\", dimension=\"swap\"})",
"fullMetaSearch": false, "fullMetaSearch": false,
"hide": false, "hide": false,
"includeNullMetadata": true, "includeNullMetadata": true,
@ -492,7 +487,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -603,7 +597,6 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -611,7 +604,7 @@
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "netdata_system_cpu_some_pressure_stall_time_ms_average{hostname=~\"$hostname\"} * -1", "expr": "netdata_system_cpu_some_pressure_stall_time_ms_average{instance=~\"$instance\"} * -1",
"hide": false, "hide": false,
"instant": false, "instant": false,
"legendFormat": "some stall time", "legendFormat": "some stall time",
@ -625,7 +618,7 @@
}, },
"disableTextWrap": false, "disableTextWrap": false,
"editorMode": "code", "editorMode": "code",
"expr": "sum by(dimension, service_name) (netdata_systemd_service_cpu_utilization_percentage_average{hostname=~\"$hostname\", service_name=~\"$service\"})", "expr": "sum by(dimension, service_name) (netdata_systemd_service_cpu_utilization_percentage_average{instance=~\"$instance\", service_name=~\"$service\"})",
"fullMetaSearch": false, "fullMetaSearch": false,
"hide": false, "hide": false,
"includeNullMetadata": true, "includeNullMetadata": true,
@ -656,7 +649,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -822,7 +814,6 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -830,7 +821,7 @@
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "netdata_system_io_full_pressure_stall_time_ms_average{hostname=~\"$hostname\"} * -1", "expr": "netdata_system_io_full_pressure_stall_time_ms_average{instance=~\"$instance\"} * -1",
"hide": false, "hide": false,
"instant": false, "instant": false,
"legendFormat": "full stall time", "legendFormat": "full stall time",
@ -887,7 +878,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -953,7 +943,6 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -961,7 +950,7 @@
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "netdata_disk_io_KiB_persec_average{hostname=~\"$hostname\", chart=~\"disk.sd.+\"}", "expr": "netdata_disk_io_KiB_persec_average{instance=~\"$instance\", chart=~\"disk.sd.+\"}",
"instant": false, "instant": false,
"legendFormat": "{{device}} / {{dimension}}", "legendFormat": "{{device}} / {{dimension}}",
"range": true, "range": true,
@ -1042,8 +1031,7 @@
"layout": "auto" "layout": "auto"
}, },
"tooltip": { "tooltip": {
"mode": "single", "show": true,
"showColorScale": false,
"yHistogram": false "yHistogram": false
}, },
"yAxis": { "yAxis": {
@ -1053,7 +1041,7 @@
"unit": "s" "unit": "s"
} }
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -1061,7 +1049,7 @@
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "{hostname=~\"$hostname\",unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 100", "expr": "{unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 100",
"legendFormat": "{{server_name}}", "legendFormat": "{{server_name}}",
"queryType": "range", "queryType": "range",
"refId": "A" "refId": "A"
@ -1157,7 +1145,6 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"barAlignment": 0, "barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "points", "drawStyle": "points",
"fillOpacity": 0, "fillOpacity": 0,
"gradientMode": "none", "gradientMode": "none",
@ -1188,8 +1175,7 @@
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "green", "color": "green"
"value": null
}, },
{ {
"color": "red", "color": "red",
@ -1224,7 +1210,7 @@
"sort": "none" "sort": "none"
} }
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -1232,7 +1218,7 @@
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "{hostname=~\"$hostname\",unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 100", "expr": "{unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 100",
"legendFormat": "", "legendFormat": "",
"queryType": "range", "queryType": "range",
"refId": "A" "refId": "A"
@ -1331,8 +1317,7 @@
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "green", "color": "green"
"value": null
}, },
{ {
"color": "red", "color": "red",
@ -1369,7 +1354,7 @@
}, },
"showHeader": true "showHeader": true
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -1377,7 +1362,7 @@
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "{hostname=~\"$hostname\",unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 1", "expr": "{unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | request_time > 1",
"queryType": "range", "queryType": "range",
"refId": "A" "refId": "A"
} }
@ -1426,8 +1411,7 @@
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "green", "color": "green"
"value": null
}, },
{ {
"color": "red", "color": "red",
@ -1488,7 +1472,7 @@
}, },
"showHeader": true "showHeader": true
}, },
"pluginVersion": "11.4.0", "pluginVersion": "10.2.0",
"targets": [ "targets": [
{ {
"datasource": { "datasource": {
@ -1496,7 +1480,7 @@
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "{hostname=~\"$hostname\",unit=\"postgresql.service\"} | regexp \".*duration: (?P<duration_ms>[0-9.]+) ms (?P<statement>.*)\" | duration_ms > 500 | __error__ != \"LabelFilterErr\"", "expr": "{unit=\"postgresql.service\"} | regexp \".*duration: (?P<duration_ms>[0-9.]+) ms (?P<statement>.*)\" | duration_ms > 500 | __error__ != \"LabelFilterErr\"",
"queryType": "range", "queryType": "range",
"refId": "A" "refId": "A"
} }
@ -1529,55 +1513,56 @@
"type": "table" "type": "table"
} }
], ],
"preload": false,
"refresh": "1m", "refresh": "1m",
"schemaVersion": 40, "schemaVersion": 38,
"tags": [], "tags": [],
"templating": { "templating": {
"list": [ "list": [
{ {
"allValue": ".*", "allValue": ".*",
"current": { "current": {
"selected": true,
"text": [ "text": [
"baryum" "127.0.0.1:19999"
], ],
"value": [ "value": [
"baryum" "127.0.0.1:19999"
] ]
}, },
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"definition": "label_values(netdata_systemd_service_unit_state_state_average,hostname)", "definition": "label_values(netdata_systemd_service_unit_state_state_average,instance)",
"hide": 0,
"includeAll": false, "includeAll": false,
"multi": true, "multi": true,
"name": "hostname", "name": "instance",
"options": [], "options": [],
"query": { "query": {
"qryType": 1, "qryType": 1,
"query": "label_values(netdata_systemd_service_unit_state_state_average,hostname)", "query": "label_values(netdata_systemd_service_unit_state_state_average,instance)",
"refId": "PrometheusVariableQueryEditor-VariableQuery" "refId": "PrometheusVariableQueryEditor-VariableQuery"
}, },
"refresh": 1, "refresh": 1,
"regex": "", "regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query" "type": "query"
}, },
{ {
"allValue": ".*", "allValue": ".*",
"current": { "current": {
"text": [ "selected": false,
"All" "text": "All",
], "value": "$__all"
"value": [
"$__all"
]
}, },
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09" "uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
}, },
"definition": "label_values(netdata_systemd_service_unit_state_state_average,unit_name)", "definition": "label_values(netdata_systemd_service_unit_state_state_average,unit_name)",
"hide": 0,
"includeAll": true, "includeAll": true,
"multi": true, "multi": true,
"name": "service", "name": "service",
@ -1589,6 +1574,8 @@
}, },
"refresh": 1, "refresh": 1,
"regex": "", "regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query" "type": "query"
} }
] ]
@ -1601,6 +1588,6 @@
"timezone": "", "timezone": "",
"title": "Performance", "title": "Performance",
"uid": "e01156bf-cdba-42eb-9845-a401dd634d41", "uid": "e01156bf-cdba-42eb-9845-a401dd634d41",
"version": 82, "version": 54,
"weekStart": "" "weekStart": ""
} }

View file

@ -1,334 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 5,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
},
"fieldConfig": {
"defaults": {},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 5,
"options": {
"dedupStrategy": "none",
"enableInfiniteScrolling": false,
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": false,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
},
"direction": "backward",
"editorMode": "code",
"expr": "{unit=~\"prometheus-.*-exporter.service\"}",
"queryType": "range",
"refId": "A"
}
],
"title": "Exporter Logs",
"type": "logs"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "red",
"mode": "shades"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "rate(net_conntrack_dialer_conn_failed_total{hostname=~\"$hostname\"}[2m]) > 0",
"instant": false,
"legendFormat": "{{dialer_name}} - {{reason}}",
"range": true,
"refId": "A"
}
],
"title": "Errors",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "red",
"mode": "thresholds"
},
"custom": {
"axisPlacement": "auto",
"fillOpacity": 70,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineWidth": 0,
"spanNulls": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"alignValue": "center",
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"mergeValues": true,
"rowHeight": 0.9,
"showValue": "never",
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "prometheus_sd_discovered_targets{hostname=~\"$hostname\"}",
"hide": false,
"instant": false,
"legendFormat": "{{config}}",
"range": true,
"refId": "All"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "label_replace(increase((sum by(dialer_name) (net_conntrack_dialer_conn_failed_total{hostname=~\"$hostname\"}))[15m:1m]), \"config\", \"$1\", \"dialer_name\", \"(.*)\")",
"hide": false,
"instant": false,
"legendFormat": "{{dialer_name}}",
"range": true,
"refId": "Failed"
}
],
"title": "Scraping jobs",
"transformations": [
{
"id": "labelsToFields",
"options": {
"keepLabels": [
"config"
],
"mode": "columns"
}
},
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"prometheus_sd_discovered_targets": true
},
"indexByName": {},
"renameByName": {
"prometheus_sd_discovered_targets": ""
}
}
},
{
"id": "partitionByValues",
"options": {
"fields": [
"config"
]
}
}
],
"type": "state-timeline"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": "baryum",
"value": "baryum"
},
"definition": "label_values(up,hostname)",
"includeAll": false,
"name": "hostname",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(up,hostname)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Scraping Jobs",
"uid": "debb763d-77aa-47bd-9290-2e02583c8ed2",
"version": 24
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

View file

@ -8,70 +8,38 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of:
- Prometheus as the database for metrics. - Prometheus as the database for metrics.
- Loki as the database for logs. - Loki as the database for logs.
## Features {#services-monitoring-features} ## Configuration {#blocks-monitoring-configuration}
- Declarative [LDAP](#blocks-monitoring-options-shb.monitoring.ldap) Configuration.
- Needed LDAP groups are created automatically.
- Declarative [SSO](#blocks-monitoring-options-shb.monitoring.sso) Configuration.
- When SSO is enabled, login with user and password is disabled.
- Registration is enabled through SSO.
- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy.
- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy.
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-monitoring-usage-applicationdashboard)
- Out of the box integration with [Scrutiny](https://github.com/AnalogJ/scrutiny) service for Hard Drives monitoring. [Manual](#blocks-monitoring-usage-scrutiny)
## Usage {#blocks-monitoring-usage}
### Initial Configuration {#blocks-monitoring-usage-configuration}
The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS,
- the [`shb.ssl` block](blocks-ssl.html#usage),
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
```nix ```nix
{
shb.monitoring = { shb.monitoring = {
enable = true; enable = true;
subdomain = "grafana"; subdomain = "grafana";
inherit domain; inherit domain;
contactPoints = [ "me@example.com" ]; contactPoints = [ "me@example.com" ];
adminPassword.result = config.shb.sops.secret."monitoring/admin_password".result; adminPasswordFile = config.sops.secrets."monitoring/admin_password".path;
secretKey.result = config.shb.sops.secret."monitoring/secret_key".result; secretKeyFile = config.sops.secrets."monitoring/secret_key".path;
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.sops.secret."monitoring/oidcSecret".result;
sharedSecretForAuthelia.result = config.shb.sops.secret."monitoring/oidcAutheliaSecret".result;
};
}; };
shb.sops.secret."monitoring/admin_password".request = config.shb.monitoring.adminPassword.request; sops.secrets."monitoring/admin_password" = {
shb.sops.secret."monitoring/secret_key".request = config.shb.monitoring.secretKey.request; sopsFile = ./secrets.yaml;
shb.sops.secret."monitoring/oidcSecret".request = config.shb.monitoring.sso.sharedSecret.request; mode = "0400";
shb.sops.secret."monitoring/oidcAutheliaSecret" = { owner = "grafana";
request = config.shb.monitoring.sso.sharedSecretForAuthelia.request; group = "grafana";
settings.key = "monitoring/oidcSecret"; restartUnits = [ "grafana.service" ];
}; };
sops.secrets."monitoring/secret_key" = {
sopsFile = ./secrets.yaml;
mode = "0400";
owner = "grafana";
group = "grafana";
restartUnits = [ "grafana.service" ];
}; };
``` ```
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
With that, Grafana, Prometheus, Loki and Promtail are setup! You can access `Grafana` at With that, Grafana, Prometheus, Loki and Promtail are setup! You can access `Grafana` at
`grafana.example.com` with user `admin` and the password from the sops key `monitoring/admin_password`. `grafana.example.com` with user `admin` and password ``.
The [user](#blocks-monitoring-options-shb.monitoring.ldap.userGroup) I recommend adding a STMP server configuration so you receive alerts by email:
and [admin](#blocks-monitoring-options-shb.monitoring.ldap.adminGroup)
LDAP groups are created automatically.
### SMTP {#blocks-monitoring-usage-smtp}
I recommend adding an SMTP server configuration so you receive alerts by email:
```nix ```nix
shb.monitoring.smtp = { shb.monitoring.smtp = {
@ -92,8 +60,6 @@ sops.secrets."monitoring/secret_key" = {
}; };
``` ```
### Log Optimization {#blocks-monitoring-usage-log-optimization}
Since all logs are now stored in Loki, you can probably reduce the systemd journal retention Since all logs are now stored in Loki, you can probably reduce the systemd journal retention
time with: time with:
@ -107,49 +73,6 @@ MaxFileSec=day
''; '';
``` ```
Other options are accessible through the upstream services modules.
You might for example want to update the metrics retention time with:
```nix
services.prometheus.retentionTime = "60d";
```
### Application Dashboard {#blocks-monitoring-usage-applicationdashboard}
Integration with the [dashboard contract](contracts-dashboard.html) is provided
by the [dashboard option](#blocks-monitoring-options-shb.monitoring.dashboard).
For example using the [Homepage](services-homepage.html) service:
```nix
{
shb.homepage.servicesGroups.Admin.services.Grafana = {
sortOrder = 10;
dashboard.request = config.shb.monitoring.dashboard.request;
};
}
```
There is also an integration for the scrutiny service, see next section.
### Scrutiny {#blocks-monitoring-usage-scrutiny}
Integration with the [Scrutiny](https://github.com/AnalogJ/scrutiny) service is enabled by default and setup automatically.
The web interface will be served under the [scrutiny.subdomain](#blocks-monitoring-options-shb.monitoring.scrutiny.subdomain) option.
If you don't want the web interface, set the option to `null`.
For integration with the [dashboard contract](contracts-dashboard.html):
```nix
{
shb.homepage.servicesGroups.Admin.services.Scrutiny = {
sortOrder = 11;
dashboard.request = config.shb.monitoring.scrutiny.dashboard.request;
};
}
```
## Provisioning {#blocks-monitoring-provisioning} ## Provisioning {#blocks-monitoring-provisioning}
Self Host Blocks will create automatically the following resources: Self Host Blocks will create automatically the following resources:
@ -178,8 +101,8 @@ This dashboard is meant to be the first stop to understand why a service is misb
![](./assets/dashboards_Errors_1.png) ![](./assets/dashboards_Errors_1.png)
![](./assets/dashboards_Errors_2.png) ![](./assets/dashboards_Errors_2.png)
The yellow and red dashed vertical bars correspond to the The yellow and red dashed vertical bars correspond to the [Requests Error Budget
[Requests Error Budget Alert](#blocks-monitoring-budget-alerts) firing. Alert](#blocks-monitoring-budget-alerts) firing.
## Performance Dashboard {#blocks-monitoring-performance-dashboard} ## Performance Dashboard {#blocks-monitoring-performance-dashboard}
@ -189,54 +112,6 @@ This dashboard is meant to be the first stop to understand why a service is perf
![Performance Dashboard Middle Part](./assets/dashboards_Performance_2.png) ![Performance Dashboard Middle Part](./assets/dashboards_Performance_2.png)
![Performance Dashboard Bottom Part](./assets/dashboards_Performance_3.png) ![Performance Dashboard Bottom Part](./assets/dashboards_Performance_3.png)
## Nextcloud Dashboard {#blocks-monitoring-nextcloud-dashboard}
See [Nextcloud service](./services-nextcloud.html#services-nextcloudserver-dashboard) manual.
## Deluge Dashboard {#blocks-monitoring-deluge-dashboard}
This dashboard is used to monitor a [deluge](./services-deluge.html) instance.
![Deluge Dashboard Top Part](./assets/dashboards_Deluge_1.png)
![Deluge Dashboard Bottom Part](./assets/dashboards_Deluge_2.png)
## Backups Dashboard and Alert {#blocks-monitoring-backup}
This dashboard shows Restic and BorgBackup backup jobs, or any job with "backup" in the systemd service name.
### Dashboard {#blocks-monitoring-backup-dashboard}
Variables:
- The "Job" variable allows to select one or more backup jobs. "All" is the default.
- The "mountpoints" variable allows to select only relevant mountpoints for backup. "All" is the default.
The most important graphs are the first three:
- "Backup Jobs in the Past Week": Shows stats on all backup jobs that ran in the past.
It is sorted by the "Failed" column in descending order.
This way, one can directly see when a job has failures.
- "Schedule": Shows when a job will run.
The unit is "Datetime from Now" meaning it shows when a job ran or will run relative to the current time.
An annotation will show up when the "Late Backups" alert fired or resolved.
- "Backup jobs": Shows when a backup job ran.
Normally, jobs running for less than 15 seconds will not show up in the graph.
We crafted a query that still shows them but the length is 15 seconds, even if the backup job
took less time to run.
![Backups Dashboard Top Part](./assets/dashboards_Backups_1.png)
![Backups Dashboard Middle Part](./assets/dashboards_Backups_2.png)
![Backups Dashboard Bottom Part](./assets/dashboards_Backups_3.png)
### Alerts {#blocks-monitoring-backup-alerts}
- The "Late Backups" alert will fire if a backup job did not run at all in the last 24 hours
or if all runs were failures in the last 24 hours.
It will show up as annotations in the "Schedule" panel of the dashboard.
![Late Backups Alert Firing](./assets/alert_rules_LateBackups_1.png)
![Backups Alert Showing Up In Dashboard](./assets/dashboards_Backups_alert.png)
## Requests Error Budget Alert {#blocks-monitoring-budget-alerts} ## Requests Error Budget Alert {#blocks-monitoring-budget-alerts}
This alert will fire when the ratio between number of requests getting a 5XX response from a service This alert will fire when the ratio between number of requests getting a 5XX response from a service
@ -244,60 +119,3 @@ and the total requests to that service exceeds 1%.
![Error Dashboard Top Part](./assets/alert_rules_5xx_1.png) ![Error Dashboard Top Part](./assets/alert_rules_5xx_1.png)
![Error Dashboard Bottom Part](./assets/alert_rules_5xx_2.png) ![Error Dashboard Bottom Part](./assets/alert_rules_5xx_2.png)
## SSL Certificates Dashboard and Alert {#blocks-monitoring-ssl}
This dashboard shows Let's Encrypt renewal and setup jobs,
or any job starting with "acme-" in the systemd service name.
### Dashboard {#blocks-monitoring-ssl-dashboard}
Variables:
- The "Job" variable allows to focus on one or more certificate. "All" is the default.
Graphs:
- "Certificate Remaining Validity": Shows in how long will certificates expire.
It shows all files under `/var/lib/acme`.
An annotation will show up when the "Certificate Did Not Renew" alert fired or resolved.
- "Schedule": Shows when a job will run.
The unit is "Datetime from Now" meaning it shows when a job ran or will run relative to the current time.
- "Jobs in the Past Week": Shows stats on all renewal jobs that ran in the past.
It is sorted by the "Failed" column in descending order.
This way, one can directly see when a job has failures.
Note, the stats is not accurate because detecting jobs taking taking less than 15 seconds
is not supported well.
- "Job Runs": Shows when a renewal job ran.
Normally, jobs running for less than 15 seconds will not show up in the graph.
We crafted a query that still shows them but the length is 100 seconds, even if the job
took less time to run.
![SSL Dashboard No Filter](./assets/dashboards_SSL_all.png)
![SSL Dashboard Filter Failing](./assets/dashboards_SSL_fail.png)
### Alerts {#blocks-monitoring-ssl-alerts}
- The "Certificate Did Not Renew" alert will fire if a backup job did not run at all in the last 24 hours
or if all runs were failures in the last 24 hours.
It will show up as annotations in the "Schedule" panel of the dashboard.
![Late SSL Jobs Alert Firing](./assets/alert_rules_LateSSL_1.png)
## Impermanence {#blocks-monitoring-impermanence}
To save the fluent-bit folder in an impermanence setup, add:
```nix
{
shb.zfs.datasets."safe/monitoring-fluent-bit".path = config.shb.jellyfin.impermanence.fluent-bit;
}
```
## Options Reference {#blocks-monitoring-options}
```{=include=} options
id-prefix: blocks-monitoring-options-
list-id: selfhostblocks-blocks-monitoring-options
source: @OPTIONS_JSON@
```

View file

@ -18,7 +18,7 @@
"uid": "cd6cc53e-840c-484d-85f7-96fede324006" "uid": "cd6cc53e-840c-484d-85f7-96fede324006"
}, },
"editorMode": "code", "editorMode": "code",
"expr": "(sum by(server_name) (count_over_time({unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | status =~ \"[1234]..\" | server_name =~ \".*\" [1h])) / sum by(server_name) (count_over_time({unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | server_name =~ \".*\" [1h])))", "expr": "(sum by(server_name) (count_over_time({unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | status =~ \"[1234]..\" | server_name =~ \".*\" [7d])) / sum by(server_name) (count_over_time({unit=\"nginx.service\"} | pattern \"<_> <_> <line>\" | line_format \"{{.line}}\" | json | __error__ != \"JSONParserErr\" | server_name =~ \".*\" [7d])))",
"intervalMs": 1000, "intervalMs": 1000,
"legendFormat": "{{server_name}}", "legendFormat": "{{server_name}}",
"maxDataPoints": 43200, "maxDataPoints": 43200,
@ -120,265 +120,10 @@
"__panelId__": "9", "__panelId__": "9",
"description": "", "description": "",
"runbook_url": "", "runbook_url": "",
"summary": "The error budget for a service for the last 1 hour is under 99%" "summary": "The error budget for a service for the last 7 days is under 99%"
},
"labels": {
"role": "sysadmin"
},
"isPaused": false
},
{
"uid": "ee817l3a88s1sd",
"title": "Certificate Did Not Renew",
"condition": "C",
"data": [
{
"refId": "A",
"relativeTimeRange": {
"from": 1800,
"to": 0
},
"datasourceUid": "df80f9f5-97d7-4112-91d8-72f523a02b09",
"model": {
"adhocFilters": [],
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "min by(subject) (ssl_certificate_expiry_seconds)",
"interval": "",
"intervalMs": 15000,
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
"maxDataPoints": 43200,
"range": true,
"refId": "A"
}
},
{
"refId": "B",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"conditions": [
{
"evaluator": {
"params": [],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"B"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "A",
"intervalMs": 1000,
"maxDataPoints": 43200,
"reducer": "last",
"refId": "B",
"type": "reduce"
}
},
{
"refId": "C",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"conditions": [
{
"evaluator": {
"params": [
604800
],
"type": "lt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"C"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "B",
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "C",
"type": "threshold"
}
}
],
"dashboardUid": "ae818js0bvw8wb",
"panelId": 3,
"noDataState": "NoData",
"execErrState": "Error",
"for": "20m",
"annotations": {
"__dashboardUid__": "ae818js0bvw8wb",
"__panelId__": "3",
"description": "The expiry date of the certificate is 1 week from now.",
"summary": "Certificate did not renew on time."
},
"labels": {
"role": "sysadmin"
},
"isPaused": false
},
{
"uid": "df4doj5pomhvkf",
"title": "Late Backups",
"condition": "C",
"data": [
{
"refId": "A",
"relativeTimeRange": {
"from": 10800,
"to": 0
},
"datasourceUid": "df80f9f5-97d7-4112-91d8-72f523a02b09",
"model": {
"adhocFilters": [],
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"exemplar": false,
"expr": "(\n # Timer triggered at least once in the last 24h\n label_replace((\n time()\n -\n systemd_timer_last_trigger_seconds{name=~\".*backup.*.timer\"}\n ) < 24*60*60, \"name\", \"$1.service\", \"name\", \"(.*).timer\")\n AND on(name)\n # At least one failure in the last 24h\n (\n max_over_time(systemd_unit_state{name=~\".*backup.*.service\", state=\"failed\"}[24h]) > 0\n )\n AND on(name)\n # No successes in the last 24h\n (\n max_over_time(systemd_unit_state{name=~\".*backup.*.service\", state=\"inactive\"}[24h]) == 0\n )\n)",
"instant": false,
"interval": "",
"intervalMs": 15000,
"legendFormat": "{{name}}",
"maxDataPoints": 43200,
"range": true,
"refId": "A"
}
},
{
"refId": "B",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"conditions": [
{
"evaluator": {
"params": [],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"B"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "A",
"intervalMs": 1000,
"maxDataPoints": 43200,
"reducer": "last",
"refId": "B",
"type": "reduce"
}
},
{
"refId": "C",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"conditions": [
{
"evaluator": {
"params": [
0
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"C"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "B",
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "C",
"type": "threshold"
}
}
],
"dashboardUid": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
"panelId": 15,
"noDataState": "OK",
"execErrState": "Error",
"annotations": {
"__dashboardUid__": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
"__panelId__": "15",
"summary": "A backup did not run in the last 24 hours."
}, },
"labels": { "labels": {
"": "",
"role": "sysadmin" "role": "sysadmin"
}, },
"isPaused": false "isPaused": false

View file

@ -1,13 +1,10 @@
{ { config, pkgs, lib, ... }:
config,
lib,
shb,
...
}:
let let
cfg = config.shb.nginx; cfg = config.shb.nginx;
contracts = pkgs.callPackage ../contracts {};
fqdn = c: "${c.subdomain}.${c.domain}"; fqdn = c: "${c.subdomain}.${c.domain}";
vhostConfig = lib.types.submodule { vhostConfig = lib.types.submodule {
@ -26,14 +23,13 @@ let
ssl = lib.mkOption { ssl = lib.mkOption {
description = "Path to SSL files"; description = "Path to SSL files";
type = lib.types.nullOr shb.contracts.ssl.certs; type = lib.types.nullOr contracts.ssl.certs;
default = null; default = null;
}; };
upstream = lib.mkOption { upstream = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.str;
description = "Upstream url to be protected."; description = "Upstream url to be protected.";
default = null;
example = "http://127.0.0.1:1234"; example = "http://127.0.0.1:1234";
}; };
@ -46,58 +42,16 @@ let
autheliaRules = lib.mkOption { autheliaRules = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.anything); type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
default = [ ];
description = "Authelia rule configuration"; description = "Authelia rule configuration";
example = lib.literalExpression '' example = lib.literalExpression ''[{
[
# Protect /admin endpoint with 2FA
# and only allow access to admin users.
{
domain = "myapp.example.com";
policy = "two_factor"; policy = "two_factor";
subject = [ "group:service_admin" ];
resources = [
"^/admin"
];
}
# Leave /api endpoint open - assumes an API key is used to protect it.
{
domain = "myapp.example.com";
policy = "bypass";
resources = [
"^/api"
];
},
# Protect rest of app with 1FA
# and allow access to normal and admin users.
{
domain = "myapp.example.com";
policy = "one_factor";
subject = ["group:service_user"]; subject = ["group:service_user"];
}, }]'';
]
'';
};
phpForwardAuth = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Authelia rule configuration";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config to add to the root / location. Strings separated by newlines.";
}; };
}; };
}; };
in in
{ {
imports = [
./authelia.nix
];
options.shb.nginx = { options.shb.nginx = {
accessLog = lib.mkOption { accessLog = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
@ -121,10 +75,7 @@ in
}; };
config = { config = {
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [ 80 443 ];
80
443
];
services.nginx.enable = true; services.nginx.enable = true;
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn"; services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";
@ -164,8 +115,7 @@ in
sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key; sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key;
# Taken from https://github.com/authelia/authelia/issues/178 # Taken from https://github.com/authelia/authelia/issues/178
locations."/".extraConfig = locations."/".extraConfig = ''
lib.optionalString (c.upstream != null) ''
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options "SAMEORIGIN"; add_header X-Frame-Options "SAMEORIGIN";
@ -185,7 +135,6 @@ in
proxy_pass ${c.upstream}; proxy_pass ${c.upstream};
'' ''
+ c.extraConfig
+ lib.optionalString (c.authEndpoint != null) '' + lib.optionalString (c.authEndpoint != null) ''
auth_request /authelia; auth_request /authelia;
auth_request_set $user $upstream_http_remote_user; auth_request_set $user $upstream_http_remote_user;
@ -209,13 +158,8 @@ in
error_page 403 = ${c.authEndpoint}/error/403; error_page 403 = ${c.authEndpoint}/error/403;
''; '';
locations."~ \\.php$".extraConfig = lib.mkIf (c.phpForwardAuth) ''
fastcgi_param HTTP_X_FORWARDED_USER $user;
fastcgi_param HTTP_X_FORWARDED_GROUPS $groups;
'';
# Virtual endpoint created by nginx to forward auth requests. # Virtual endpoint created by nginx to forward auth requests.
locations."/authelia".extraConfig = lib.mkIf (c.authEndpoint != null) '' locations."/authelia".extraConfig = lib.mkIf (!(isNull c.authEndpoint)) ''
internal; internal;
proxy_pass ${c.authEndpoint}/api/verify; proxy_pass ${c.authEndpoint}/api/verify;

View file

@ -1,211 +0,0 @@
# Nginx Block {#blocks-nginx}
Defined in [`/modules/blocks/nginx.nix`](@REPO@/modules/blocks/nginx.nix).
This block sets up a [Nginx](https://nginx.org/) instance.
It complements the upstream nixpkgs with some authentication and debugging improvements as shows in the Usage section.
## Usage {#blocks-nginx-usage}
### Access Logging {#blocks-nginx-usage-accesslog}
JSON access logging is enabled with the [`shb.nginx.accessLog`](#blocks-nginx-options-shb.nginx.accessLog) option:
```nix
{
shb.nginx.accessLog = true;
}
```
Looking at the systemd logs (`journalctl -fu nginx`) will show for example:
```json
nginx[969]: server nginx:
{
"remote_addr":"192.168.1.1",
"remote_user":"-",
"time_local":"29/Dec/2025:14:22:41 +0000",
"request":"POST /api/firstfactor HTTP/2.0",
"request_length":"264",
"server_name":"auth_example_com",
"status":"200",
"bytes_sent":"855",
"body_bytes_sent":"60",
"referrer":"-",
"user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0",
"gzip_ration":"-",
"post":"{\x22username\x22:\x22charlie\x22,\x22password\x22:\x22CharliePassword\x22,\x22keepMeLoggedIn\x22:false,\x22targetURL\x22:\x22https://f.example.com/\x22,\x22requestMethod\x22:null}",
"upstream_addr":"127.0.0.1:9091",
"upstream_status":"200",
"request_time":"0.873",
"upstream_response_time":"0.873",
"upstream_connect_time":"0.001",
"upstream_header_time":"0.872"
}
```
This _will_ log the body of POST queries so it should only be enabled for debug logging.
### Debug Logging {#blocks-nginx-usage-debuglog}
Debug logging is enabled with the [`shb.nginx.debugLog`](#blocks-nginx-options-shb.nginx.debugLog) option:
```nix
{
shb.nginx.debugLog = true;
}
```
If enabled, it sets:
```
error_log stderr warn;
```
### Virtual Host Upstream Proxy {#blocks-nginx-usage-upstream}
Easy upstream proxy setup is done with the [`shb.nginx.vhosts.*.upstream`](#blocks-nginx-options-shb.nginx.vhosts._.upstream) option:
```nix
{
shb.nginx.vhosts = [
{
domain = "example.com";
subdomain = "mysubdomain";
upstream = "http://127.0.0.1:9090";
}
];
}
```
This will set also a few headers.
Some are shown here and others please see in the [nginx](@REPO@/modules/blocks/nginx.nix) module:
- `Host` = `$host`;
- `X-Real-IP` = `$remote_addr`;
- `X-Forwarded-For` = `$proxy_add_x_forwarded_for`;
- `X-Forwarded-Proto` = `$scheme`;
### Virtual Host SSL Generator Contract Integration {#blocks-nginx-usage-ssl}
This module integrates with the [SSL Generator Contract](./contracts-ssl.html)
to setup HTTPs with the [`shb.nginx.vhosts.*.ssl`](#blocks-nginx-options-shb.nginx.vhosts._.ssl) option:
```nix
{
shb.nginx.vhosts = [
{
domain = "example.com";
subdomain = "mysubdomain";
ssl = config.shb.certs.certs.letsencrypt.${domain};;
}
];
shb.certs.certs.letsencrypt.${domain} = {
inherit domain;
};
}
```
### Virtual Host SHB Forward Authentication {#blocks-nginx-usage-shbforwardauth}
For services provided by SelfHostBlocks that do not handle [OIDC integration][OIDC],
this block can provide [forward authentication][] which still allows the service
to still be protected by an SSO server.
[OIDC]: blocks-authelia.html#blocks-authelia-shb-oidc
The user could still be required to authenticate to the service itself,
although some services can automatically users authorized by Authelia.
[forward authentication]: https://doc.traefik.io/traefik/middlewares/http/forwardauth/
Integrating with this block is done with the following code:
```nix
shb.<services>.authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
```
### Virtual Host Forward Authentication {#blocks-nginx-usage-forwardauth}
Forward authentication is when Nginx talks with the SSO service directly
and the user is authenticated before reaching the upstream application.
The SSO service responds with the username, group and more information about the user.
This is then forwarded to the upstream application by Nginx.
Note that _every_ request is authenticated this way with the SSO server
so it involves more hops than a direct [OIDC integration](blocks-authelia.html#blocks-authelia-shb-oidc).
```nix
{
shb.nginx.vhosts = [
{
domain = "example.com";
subdomain = "mysubdomain";
authEndpoint = "authelia.example.com";
autheliaRules = [
[
# Protect /admin endpoint with 2FA
# and only allow access to admin users.
{
domain = "myapp.example.com";
policy = "two_factor";
subject = [ "group:service_admin" ];
resources = [
"^/admin"
];
}
# Leave /api endpoint open - assumes an API key is used to protect it.
{
domain = "myapp.example.com";
policy = "bypass";
resources = [
"^/api"
];
},
# Protect rest of app with 1FA
# and allow access to normal and admin users.
{
domain = "myapp.example.com";
policy = "one_factor";
subject = ["group:service_user"];
},
]
];
}
];
}
```
If PHP is used with fastCGI,
extra headers must be added by enabling the [`shb.nginx.vhosts.*.phpForwardAuth`](#blocks-nginx-options-shb.nginx.vhosts._.phpForwardAuth) option.
### Virtual Host Extra Config {#blocks-nginx-usage-extraconfig}
To add extra configuration to a virtual host,
use the [`shb.nginx.vhosts.*.extraConfig`](#blocks-nginx-options-shb.nginx.vhosts._.extraConfig) option.
This can be used to add headers, for example:
```nix
{
shb.nginx.vhosts = [
{
domain = "example.com";
subdomain = "mysubdomain";
extraConfig = ''
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
'';
}
];
}
```
## Options Reference {#blocks-nginx-options}
```{=include=} options
id-prefix: blocks-nginx-options-
list-id: selfhostblocks-block-nginx-options
source: @OPTIONS_JSON@
```

View file

@ -1,15 +1,8 @@
{ { config, lib, pkgs, ... }:
config,
lib,
pkgs,
shb,
...
}:
let let
cfg = config.shb.postgresql; cfg = config.shb.postgresql;
upgrade-script = upgrade-script = old: new:
old: new:
let let
oldStr = builtins.toString old; oldStr = builtins.toString old;
newStr = builtins.toString new; newStr = builtins.toString new;
@ -39,10 +32,6 @@ let
''; '';
in in
{ {
imports = [
../../lib/module.nix
];
options.shb.postgresql = { options.shb.postgresql = {
debug = lib.mkOption { debug = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
@ -60,33 +49,9 @@ in
default = false; default = false;
}; };
databasebackup = lib.mkOption {
description = ''
Backup configuration.
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.databasebackup.mkRequester {
user = "postgres";
backupName = "postgres.sql";
backupCmd = ''
${pkgs.postgresql}/bin/pg_dumpall --clean --if-exists | ${pkgs.gzip}/bin/gzip --rsyncable
'';
restoreCmd = ''
${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres
'';
};
};
};
ensures = lib.mkOption { ensures = lib.mkOption {
description = "List of username, database and/or passwords that should be created."; description = "List of username, database and/or passwords that should be created.";
type = lib.types.listOf ( type = lib.types.listOf (lib.types.submodule {
lib.types.submodule {
options = { options = {
username = lib.mkOption { username = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -105,8 +70,7 @@ in
example = "/run/secrets/postgresql/password"; example = "/run/secrets/postgresql/password";
}; };
}; };
} });
);
default = []; default = [];
}; };
}; };
@ -114,8 +78,6 @@ in
config = config =
let let
commonConfig = { commonConfig = {
systemd.services.postgresql.serviceConfig.Restart = "always";
services.postgresql.settings = { services.postgresql.settings = {
}; };
}; };
@ -135,21 +97,18 @@ in
dbConfig = ensureCfgs: { dbConfig = ensureCfgs: {
services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0); services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0);
services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs; services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs;
services.postgresql.ensureUsers = map ( services.postgresql.ensureUsers = map ({ username, database, ... }: {
{ username, database, ... }:
{
name = username; name = username;
ensureDBOwnership = true; ensureDBOwnership = true;
ensureClauses.login = true; ensureClauses.login = true;
} }) ensureCfgs;
) ensureCfgs;
}; };
pwdConfig = ensureCfgs: { pwdConfig = ensureCfgs: {
systemd.services.postgresql-setup.script = lib.mkAfter ( systemd.services.postgresql.postStart =
let let
prefix = '' prefix = ''
psql -tA <<'EOF' $PSQL -tA <<'EOF'
DO $$ DO $$
DECLARE password TEXT; DECLARE password TEXT;
BEGIN BEGIN
@ -158,24 +117,17 @@ in
END $$; END $$;
EOF EOF
''; '';
exec = exec = { username, passwordFile, ... }: ''
{ username, passwordFile, ... }:
''
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', ''')); password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
EXECUTE format('ALTER ROLE "${username}" WITH PASSWORD '''%s''';', password); EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
''; '';
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs; cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
in in
if (builtins.length cfgsWithPasswords) == 0 then if (builtins.length cfgsWithPasswords) == 0 then "" else
"" prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix;
else
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix
);
}; };
debugConfig = debugConfig = enableDebug: lib.mkIf enableDebug {
enableDebug:
lib.mkIf enableDebug {
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements"; services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
}; };
in in
@ -187,8 +139,9 @@ in
(debugConfig cfg.debug) (debugConfig cfg.debug)
{ {
environment.systemPackages = lib.mkIf config.services.postgresql.enable [ environment.systemPackages = lib.mkIf config.services.postgresql.enable [
(upgrade-script 13 14)
(upgrade-script 14 15)
(upgrade-script 15 16) (upgrade-script 15 16)
(upgrade-script 16 17)
]; ];
} }
]); ]);

View file

@ -1,72 +0,0 @@
# PostgreSQL Block {#blocks-postgresql}
Defined in [`/modules/blocks/postgresql.nix`](@REPO@/modules/blocks/postgresql.nix).
This block sets up a [PostgreSQL][] database.
[postgresql]: https://www.postgresql.org/
Compared to the upstream nixpkgs module, this module also sets up:
- Enabling TCP/IP login and also accepting password authentication from localhost with [`shb.postgresql.enableTCPIP`](#blocks-postgresql-options-shb.postgresql.enableTCPIP).
- Enhance the `ensure*` upstream option by setting up a database's password from a password file with [`shb.postgresql.ensures`](#blocks-postgresql-options-shb.postgresql.ensures).
- Debug logging with `auto_explain` and `pg_stat_statements` with [`shb.postgresql.debug`](#blocks-postgresql-options-shb.postgresql.debug).
## Usage {#blocks-postgresql-usage}
### Ensure User and Database {#blocks-postgresql-ensures}
Ensure a database and user exists:
```nix
shb.postgresql.ensures = [
{
username = "firefly-iii";
database = "firefly-iii";
}
];
```
Also set up the database password from a file path:
```nix
shb.postgresql.ensures = [
{
username = "firefly-iii";
database = "firefly-iii";
passwordFile = "/run/secrets/firefly-iii_db_password";
}
];
```
### Database Backup Requester Contracts {#blocks-postgresql-contract-databasebackup}
This block can be backed up using the [database backup](contracts-databasebackup.html) contract.
Contract integration tests are defined in [`/test/contracts/databasebackup.nix`](@REPO@/test/contracts/databasebackup.nix).
#### Backing up All Databases {#blocks-postgresql-contract-databasebackup-all}
```nix
{
my.backup.provider."postgresql" = {
request = config.shb.postgresql.databasebackup;
settings = {
// Specific options for the backup provider.
};
};
}
```
## Tests {#blocks-postgresql-tests}
Specific integration tests are defined in [`/test/blocks/postgresql.nix`](@REPO@/test/blocks/postgresql.nix).
## Options Reference {#blocks-postgresql-options}
```{=include=} options
id-prefix: blocks-postgresql-options-
list-id: selfhostblocks-block-postgresql-options
source: @OPTIONS_JSON@
```

View file

@ -1,484 +0,0 @@
{
config,
pkgs,
lib,
shb,
utils,
...
}:
let
cfg = config.shb.restic;
inherit (lib)
concatStringsSep
filterAttrs
flatten
literalExpression
optionals
listToAttrs
mapAttrsToList
mkEnableOption
mkOption
mkMerge
;
inherit (lib)
hasPrefix
mkIf
nameValuePair
optionalAttrs
removePrefix
;
inherit (lib.types)
attrsOf
enum
int
ints
oneOf
nonEmptyStr
nullOr
str
submodule
;
commonOptions =
{
name,
prefix,
config,
...
}:
{
enable = mkEnableOption ''
SelfHostBlocks' Restic block
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
'';
passphrase = lib.mkOption {
description = "Encryption key for the backup repository.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = config.request.user;
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
restartUnits = [ "${fullName name config.settings.repository}.service" ];
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
};
};
};
repository = mkOption {
description = "Repositories to back this instance to.";
type = submodule {
options = {
path = mkOption {
type = str;
description = "Repository location";
};
secrets = mkOption {
type = attrsOf shb.secretFileType;
default = { };
description = ''
Secrets needed to access the repository where the backups will be stored.
See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
'';
example = literalExpression ''
{
AWS_ACCESS_KEY_ID.source = <path/to/secret>;
AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
}
'';
};
timerConfig = mkOption {
type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
};
description = "When to run the backup. See {manpage}`systemd.timer(5)` for details.";
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
};
};
};
retention = mkOption {
description = "For how long to keep backup files.";
type = attrsOf (oneOf [
int
nonEmptyStr
]);
default = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
limitUploadKiBs = mkOption {
type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
limitDownloadKiBs = mkOption {
type = nullOr int;
description = "Limit download bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
};
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
in
{
imports = [
../../lib/module.nix
../blocks/monitoring.nix
];
options.shb.restic = {
enableDashboard = lib.mkEnableOption "the Backups SHB dashboard" // {
default = true;
};
instances = mkOption {
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = shb.contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "instances";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
databases = mkOption {
description = "Databases to backup following the [database backup contract](./shb.contracts-databasebackup.html).";
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = shb.contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "databases";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = mkOption {
description = "Reduce performance impact of backup jobs.";
default = { };
type = submodule {
options = {
niceness = mkOption {
type = ints.between (-20) 19;
description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process";
default = 15;
};
ioSchedulingClass = mkOption {
type = enum [
"idle"
"best-effort"
"realtime"
];
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = "best-effort";
};
ioPriority = mkOption {
type = nullOr (ints.between 0 7);
description = "ionice priority, defaults to 7 for lowest priority IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = 7;
};
};
};
};
};
config = mkIf (cfg.instances != { } || cfg.databases != { }) (
let
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
in
mkMerge [
{
environment.systemPackages = optionals (enabledInstances != { } || enabledDatabases != { }) [
pkgs.restic
];
}
{
# Create repository if it is a local path.
systemd.tmpfiles.rules =
let
mkSettings =
name: instance:
optionals (hasPrefix "/" instance.settings.repository.path) [
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
];
in
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
}
{
services.restic.backups =
let
mkSettings = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
repository = instance.settings.repository.path;
paths = instance.request.sourceDirectories;
passwordFile = toString instance.settings.passphrase.result.path;
initialize = true;
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup;
backupCleanupCommand = concatStringsSep "\n" instance.request.hooks.afterBackup;
extraBackupArgs =
(optionals (instance.settings.limitUploadKiBs != null) [
"--limit-upload=${toString instance.settings.limitUploadKiBs}"
])
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
]);
}
// optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
exclude = instance.request.excludePatterns;
};
};
in
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
}
{
services.restic.backups =
let
mkSettings = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
repository = instance.settings.repository.path;
dynamicFilesFrom = "echo";
passwordFile = toString instance.settings.passphrase.result.path;
initialize = true;
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
extraBackupArgs =
(optionals (instance.settings.limitUploadKiBs != null) [
"--limit-upload=${toString instance.settings.limitUploadKiBs}"
])
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
])
++ (
let
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
in
[
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
]
);
};
};
in
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
}
{
systemd.services =
let
mkSettings =
name: instance:
let
serviceName = fullName name instance.settings.repository;
in
{
${serviceName} = mkMerge [
{
serviceConfig = {
Nice = cfg.performance.niceness;
IOSchedulingClass = cfg.performance.ioSchedulingClass;
IOSchedulingPriority = cfg.performance.ioPriority;
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != { }) {
serviceConfig.EnvironmentFile = [
"/run/secrets_restic/${serviceName}"
];
after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ];
})
];
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
let
script = shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_restic/${serviceName}";
generator = shb.toEnvVar;
user = instance.request.user;
};
in
{
script = script.preStart;
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
}
);
};
in
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
}
{
systemd.services =
let
mkEnv =
name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
# Purposely not a oneshot systemd service otherwise
# the service waits on the completion the backup before deactivating.
# This seems like a nice property at first but there is one annoying
# edge case when deploying. If a backup job somehow is started when
# the deploy happens, the deploy will wait on the service to finish
# before considering the deploy done. And worse, it will consider the
# deploy as failed if the backup fails, which is not what you want.
script = (
shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
RESTIC_REPOSITORY = instance.settings.repository.path;
};
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
generator = shb.toEnvVar;
user = instance.request.user;
}
);
};
in
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
}
{
environment.systemPackages =
let
mkResticBinary =
name: instance:
shb.contracts.backup.mkRestoreScript {
name = fullName name instance.settings.repository;
user = instance.request.user;
sudoPreserveEnvs = [
"RESTIC_REPOSITORY"
"RESTIC_PASSWORD_FILE"
];
secretsFile = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
restoreCmd = ''${pkgs.restic}/bin/restic restore \"$snapshot\" --target /'';
listCmd = ''if [ -e \"$RESTIC_REPOSITORY/index\" ]; then ${pkgs.restic}/bin/restic snapshots --json | ${pkgs.jq}/bin/jq '.[].id'; fi'';
};
in
flatten (mapAttrsToList mkResticBinary cfg.instances);
}
{
environment.systemPackages =
let
mkResticBinary =
name: instance:
shb.contracts.backup.mkRestoreScript {
name = fullName name instance.settings.repository;
user = instance.request.user;
sudoPreserveEnvs = [
"RESTIC_REPOSITORY"
"RESTIC_PASSWORD_FILE"
];
secretsFile = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
restoreCmd = ''${pkgs.restic}/bin/restic dump \"$snapshot\" ${instance.request.backupName} | ${instance.request.restoreCmd}'';
listCmd = ''if [ -e \"$RESTIC_REPOSITORY/index\" ]; then ${pkgs.restic}/bin/restic snapshots --json | ${pkgs.jq}/bin/jq '.[].id'; fi'';
};
in
flatten (mapAttrsToList mkResticBinary cfg.databases);
}
(lib.mkIf (cfg.enableDashboard && (cfg.instances != { } || cfg.databases != { })) {
shb.monitoring.dashboards = [
./backup/dashboard/Backups.json
];
})
]
);
}

View file

@ -1,305 +0,0 @@
# Restic Block {#blocks-restic}
Defined in [`/modules/blocks/restic.nix`](@REPO@/modules/blocks/restic.nix).
This block sets up a backup job using [Restic][].
It is heavily based on the nixpkgs Restic module.
[restic]: https://restic.net/
## Provider Contracts {#blocks-restic-contract-provider}
This block provides the following contracts:
- [backup contract](contracts-backup.html) under the [`shb.restic.instances`][instances] option.
It is tested with [contract tests][backup contract tests].
- [database backup contract](contracts-databasebackup.html) under the [`shb.restic.databases`][databases] option.
It is tested with [contract tests][database backup contract tests].
[instances]: #blocks-restic-options-shb.restic.instances
[databases]: #blocks-restic-options-shb.restic.databases
[backup contract tests]: @REPO@/test/contracts/backup.nix
[database backup contract tests]: @REPO@/test/contracts/databasebackup.nix
As requested by those two contracts, when setting up a backup with Restic,
a backup Systemd service and a [restore script](#blocks-restic-maintenance) are provided.
## Usage {#blocks-restic-usage}
The following examples assume usage of the [sops block][] to provide secrets
although any blocks providing the [secrets contract][] works too.
[sops block]: ./blocks-sops.html
[secrets contract]: ./contracts-secrets.html
### One folder backed up manually {#blocks-restic-usage-provider-manual}
The following snippet shows how to configure
the backup of 1 folder to 1 repository.
We assume that the folder `/var/lib/myfolder` of the service `myservice` must be backed up.
```nix
shb.restic.instances."myservice" = {
request = {
user = "myservice";
sourceDirectories = [
"/var/lib/myfolder"
];
};
settings = {
enable = true;
passphrase.result = config.shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
config.shb.restic.instances."myservice".settings.passphrase.request;
```
### One folder backed up with contract {#blocks-restic-usage-provider-contract}
With the same example as before but assuming the `myservice` service
has a `myservice.backup` option that is a requester for the backup contract,
the snippet above becomes:
```nix
shb.restic.instances."myservice" = {
request = config.myservice.backup.request;
settings = {
enable = true;
passphrase.result = config.shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
config.shb.restic.instances."myservice".settings.passphrase.request;
```
### One folder backed up to S3 {#blocks-restic-usage-provider-remote}
Here we will only highlight the differences with the previous configuration.
This assumes you have access to such a remote S3 store, for example by using [Backblaze](https://www.backblaze.com/).
```diff
shb.test.backup.instances.myservice = {
repository = {
- path = "/srv/pool1/backups/myfolder";
+ path = "s3:s3.us-west-000.backblazeb2.com/backups/myfolder";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
+ extraSecrets = {
+ AWS_ACCESS_KEY_ID.source="<path/to/access_key_id>";
+ AWS_SECRET_ACCESS_KEY.source="<path/to/secret_access_key>";
+ };
};
}
```
### Multiple directories to multiple destinations {#blocks-restic-usage-multiple}
The following snippet shows how to configure backup of any number of folders to 3 repositories,
each happening at different times to avoid I/O contention.
We will also make sure to be able to re-use as much as the configuration as possible.
A few assumptions:
- 2 hard drive pools used for backup are mounted respectively on `/srv/pool1` and `/srv/pool2`.
- You have a backblaze account.
First, let's define a variable to hold all the repositories we want to back up to:
```nix
repos = [
{
path = "/srv/pool1/backups";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "/srv/pool2/backups";
timerConfig = {
OnCalendar = "08:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "s3:s3.us-west-000.backblazeb2.com/backups";
timerConfig = {
OnCalendar = "16:00:00";
RandomizedDelaySec = "3h";
};
}
];
```
Compared to the previous examples, we do not include the name of what we will back up in the
repository paths.
Now, let's define a function to create a backup configuration. It will take a list of repositories,
a name identifying the backup and a list of folders to back up.
```nix
backupcfg = repositories: name: sourceDirectories {
enable = true;
backend = "restic";
keySopsFile = ../secrets/backup.yaml;
repositories = builtins.map (r: {
path = "${r.path}/${name}";
inherit (r) timerConfig;
}) repositories;
inherit sourceDirectories;
retention = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
environmentFile = true;
};
```
Now, we can define multiple backup jobs to backup different folders:
```nix
shb.test.backup.instances.myfolder1 = backupcfg repos ["/var/lib/myfolder1"];
shb.test.backup.instances.myfolder2 = backupcfg repos ["/var/lib/myfolder2"];
```
The difference between the above snippet and putting all the folders into one configuration (shown
below) is the former splits the backups into sub-folders on the repositories.
```nix
shb.test.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/myfolder2"];
```
## Monitoring {#blocks-restic-monitoring}
A generic dashboard for all backup solutions is provided.
See [Backups Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-backup) section in the monitoring chapter.
## Maintenance {#blocks-restic-maintenance}
### Manual Backup {#blocks-restic-maintenance-manuql}
To launch a backup manually, just run:
```bash
systemctl start <systemd-service-name>
```
You can easily discover the systemd service name you need by either listing the units:
```bash
systemctl list-units 'restic*'
```
Or by autocompleting the unit name with `<TAB>`:
```bash
systemctl start restic<TAB><TAB>
```
Note that the systemd services are of `Type=simple` which means the systemd service
will not wait for the backup completion to terminate.
If you want instead to wait for the backup to complete, use the `--wait` flag:
```bash
systemctl start --wait <systemd-service-name>
```
### Restore {#blocks-restic-maintenance-restore}
One command-line helper is provided per backup instance and repository pair which allows to:
- list snapshots: `<script> snapshots`
- to restore a snapshot: `<script> restore <snapshot>`
The restore script has all the secrets needed to access the repo,
it will run `sudo` automatically
and the user running it needs to have correct permissions for privilege escalation.
In the [multiple directories example](#blocks-restic-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
```bash
restic-myfolder1_srv_pool1_backups
restic-myfolder1_srv_pool2_backups
restic-myfolder1_s3_s3.us-west-000.backblazeb2.com_backups
restic-myfolder2_srv_pool1_backups
restic-myfolder2_srv_pool2_backups
restic-myfolder2_s3_s3.us-west-000.backblazeb2.com_backups
```
Discovering those is easy thanks to tab-completion.
One can then restore a backup from a given repository with:
```bash
restic-myfolder1_srv_pool1_backups restore latest
```
### Troubleshooting {#blocks-restic-maintenance-troubleshooting}
In case something bad happens with a backup, the [official documentation](https://restic.readthedocs.io/en/stable/077_troubleshooting.html) has a lot of tips.
## Tests {#blocks-restic-tests}
Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/test/blocks/restic.nix).
## Options Reference {#blocks-restic-options}
```{=include=} options
id-prefix: blocks-restic-options-
list-id: selfhostblocks-block-restic-options
source: @OPTIONS_JSON@
```

View file

@ -1,3 +0,0 @@
{ lib, ... }:
{
}

View file

@ -1,164 +0,0 @@
{
config,
lib,
pkgs,
shb,
utils,
...
}:
let
cfg = config.shb.sanoid;
restoreScriptName = name: "sanoid-${utils.escapeSystemdPath name}-restore";
backupScriptBase = "sanoid";
restoreScript =
name:
pkgs.writers.writePython3Bin (restoreScriptName name)
{
flakeIgnore = [ "E501" ];
}
''
import argparse
import subprocess
import sys
dataset = "${name}"
class ZFSError(Exception):
pass
def run_command(cmd: list[str]) -> str:
try:
result = subprocess.run(
cmd,
check=True,
text=True,
capture_output=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise ZFSError(
f"Command failed: {' '.join(cmd)}\n"
f"Exit code: {e.returncode}\n"
f"stderr: {e.stderr.strip()}"
) from e
def list_snapshots() -> None:
"""List all ZFS snapshots."""
output = run_command(["zfs", "list", "-H", "-t", "snapshot", dataset])
if not output:
return []
return output.splitlines()
def restore_snapshot(snapshot: str) -> None:
"""Rollback to a given snapshot."""
if not snapshot:
raise ValueError("Snapshot name must not be empty")
print(f"Rolling back to snapshot: {snapshot}")
run_command(["zfs", "rollback", "-r", snapshot])
print("Rollback completed successfully.")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=f"Restore script for {dataset}")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser(
"snapshots",
help="List all ZFS snapshots",
)
restore_parser = subparsers.add_parser(
"restore",
help="Rollback to a specific snapshot",
)
restore_parser.add_argument(
"snapshot",
help="Snapshot name (e.g. pool/dataset@snapname)",
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
try:
if args.command == "snapshots":
snapshots = list_snapshots()
for s in snapshots:
print(s)
elif args.command == "restore":
restore_snapshot(args.snapshot)
else:
parser.print_help()
sys.exit(1)
except ZFSError as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
'';
in
{
imports = [
../../lib/module.nix
];
options.shb.sanoid.backup = lib.mkOption {
description = "Sanoid prodiver for file backup contract";
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = shb.contracts.backup.mkProvider {
resultCfg = {
restoreScript = restoreScriptName name;
restoreScriptText = restoreScriptName "<name>";
backupService = "${backupScriptBase}.service";
backupServiceText = "${backupScriptBase}.service";
};
settings = lib.mkOption {
description = "Options passed to the `services.sanoid.datasets.<name>` option.";
default = { };
type = lib.types.attrsOf lib.types.anything;
};
};
}
)
);
};
config = lib.mkIf (cfg.backup != { }) {
services.sanoid.enable = true;
services.sanoid.datasets =
let
mkDataset = name: cfg': {
inherit name;
value = cfg'.settings;
};
in
lib.mapAttrs' mkDataset cfg.backup;
environment.systemPackages = map restoreScript (lib.attrNames cfg.backup);
};
}

View file

@ -1,97 +0,0 @@
# Sanoid Block {#blocks-sanoid}
Defined in [`/modules/blocks/sanoid.nix`](@REPO@/modules/blocks/sanoid.nix):
```nix
{
imports = [
inputs.selfhostblocks.nixosModules.sanoid
];
}
```
## Provider Contracts {#blocks-sanoid-contract-provider}
This block provides the following contracts:
- [dataset backup contract](contracts-datasetbackup.html) under the [`shb.sanoid.backup`][backup] option.
It is tested with the [generic contract tests][backup contract tests].
[backup]: #blocks-sanoid-options-shb.sanoid.backup
[backup contract tests]: @REPO@/test/contracts/backup.nix
## Usage {#blocks-sanoid-usage}
Sanoid uses templates to know when snapshots should be kept or pruned.
### Default Template {#blocks-sanoid-usage-default-template}
Backup a dataset using the default Sanoid template:
```nix
{
shb.zfs.pools.root.datasets.home = {
path = "/home";
};
shb.sanoid.backup."root/home" = {
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
};
}
```
This uses the dataset backup contract which is exposed through the ZFS module's [`shb.zfs.pools.<name>.datasets.<name>.datasetBackup`](blocks-zfs.html#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup) option.
### Custom Template {#blocks-sanoid-usage-custom-template}
Create a custom template and use it:
```nix
{
shb.zfs.pools.root.datasets.home = {
path = "/home";
};
services.sanoid.templates."yearly" = {
hourly = 10;
daily = 3;
monthly = 3;
yearly = 2;
};
shb.sanoid.backup."root/home" = {
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
template = "yearly";
};
}
```
Note we use the upstream `services.sanoid.templates` option to define the templates.
### Without contract {#blocks-sanoid-usage-without-contract}
To backup a dataset that does not provide the dataset backup contract,
we can just set the request manually:
```nix
{
shb.zfs.pools.root.datasets.home = {
path = "/home";
};
shb.sanoid.backup."root/home" = {
request.dataset = "root/home";
};
}
```
Note the attr name under the `shb.sanoid.backup` option does not
set the dataset name.
## Options Reference {#blocks-sanoid-options}
```{=include=} options
id-prefix: blocks-sanoid-options-
list-id: selfhostblocks-blocks-sanoid-options
source: @OPTIONS_JSON@
```

View file

@ -1,59 +0,0 @@
{
config,
lib,
shb,
...
}:
let
inherit (lib) mapAttrs mkOption;
inherit (lib.types) attrsOf anything submodule;
cfg = config.shb.sops;
in
{
imports = [
../../lib/module.nix
];
options.shb.sops = {
secret = mkOption {
description = "Secret following the [secret contract](./contracts-secret.html).";
default = { };
type = attrsOf (
submodule (
{ name, options, ... }:
{
options = shb.contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Sops provider.
This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix).
Note though that the `mode`, `owner`, `group`, and `restartUnits`
are managed by the [shb.sops.secret.<name>.request](#blocks-sops-options-shb.sops.secret._name_.request) option.
'';
type = attrsOf anything;
default = { };
};
resultCfg = {
path = "/run/secrets/${name}";
pathText = "/run/secrets/<name>";
};
};
}
)
);
};
};
config = {
sops.secrets =
let
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
in
mapAttrs mkSecret cfg.secret;
};
}

View file

@ -1,60 +0,0 @@
# SOPS Block {#blocks-sops}
Defined in [`/modules/blocks/sops.nix`](@REPO@/modules/blocks/sops.nix).
This block sets up a [sops-nix][] secret.
It is only a small layer on top of `sops-nix` options
to adapt it to the [secret contract](./contracts-secret.html).
[sops-nix]: https://github.com/Mic92/sops-nix
## Provider Contracts {#blocks-sops-contract-provider}
This block provides the following contracts:
- [secret contract][] under the [`shb.sops.secret`][secret] option.
It is not yet tested with [contract tests][secret contract tests] but it is used extensively on several machines.
[secret]: #blocks-sops-options-shb.sops.secret
[secret contract]: contracts-secret.html
[secret contract tests]: @REPO@/test/contracts/secret.nix
As requested by the contract, when asking for a secret with the `shb.sops` module,
the path where the secret will be located can be found under the [`shb.sops.secret.<name>.result`][result] option.
[result]: #blocks-sops-options-shb.sops.secret._name_.result
## Usage {#blocks-sops-usage}
First, a file with encrypted secrets must be created by following the [secrets setup section](usage.html#usage-secrets).
### With Requester Module {#blocks-sops-usage-requester}
This example shows how to use this sops block
to fulfill the request of a module using the [secret contract][] under the option `services.mymodule.mysecret`.
```nix
shb.sops.secret."mymodule/mysecret".request = config.services.mymodule.mysecret.request;
services.mymodule.mysecret.result = config.shb.sops.secret."mymodule/mysecret".result;
```
### Manual Module {#blocks-sops-usage-manual}
The provider module can be used on its own, without a requester module:
```nix
shb.sops.secret."mymodule/mysecret".request = {
mode = "0400";
owner = "owner";
};
services.mymodule.mysecret.path = config.sops.secret."mymodule/mysecret".result.path;
```
## Options Reference {#blocks-sops-options}
```{=include=} options
id-prefix: blocks-sops-options-
list-id: selfhostblocks-block-sops-options
source: @OPTIONS_JSON@
```

View file

@ -1,29 +1,11 @@
{ { config, pkgs, lib, ... }:
config,
pkgs,
lib,
shb,
...
}:
let let
cfg = config.shb.certs; cfg = config.shb.certs;
inherit (builtins) dirOf; contracts = pkgs.callPackage ../contracts {};
inherit (lib)
flatten
mapAttrsToList
optionalAttrs
optionals
unique
;
in in
{ {
imports = [
../../lib/module.nix
./monitoring.nix
];
options.shb.certs = { options.shb.certs = {
systemdService = lib.mkOption { systemdService = lib.mkOption {
description = '' description = ''
@ -32,16 +14,10 @@ in
type = lib.types.str; type = lib.types.str;
default = "shb-ca-bundle.service"; default = "shb-ca-bundle.service";
}; };
enableDashboard = lib.mkEnableOption "the SSL SHB dashboard" // {
default = true;
};
cas.selfsigned = lib.mkOption { cas.selfsigned = lib.mkOption {
description = "Generate a self-signed Certificate Authority."; description = "Generate a self-signed Certificate Authority.";
default = {}; default = {};
type = lib.types.attrsOf ( type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: {
lib.types.submodule (
{ config, ... }:
{
options = { options = {
name = lib.mkOption { name = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -58,8 +34,8 @@ in
This option implements the SSL Generator contract. This option implements the SSL Generator contract.
''; '';
type = shb.contracts.ssl.certs-paths; type = contracts.ssl.certs-paths;
default = { default = rec {
key = "/var/lib/certs/cas/${config._module.args.name}.key"; key = "/var/lib/certs/cas/${config._module.args.name}.key";
cert = "/var/lib/certs/cas/${config._module.args.name}.cert"; cert = "/var/lib/certs/cas/${config._module.args.name}.cert";
}; };
@ -75,20 +51,15 @@ in
default = "shb-certs-ca-${config._module.args.name}.service"; default = "shb-certs-ca-${config._module.args.name}.service";
}; };
}; };
} }));
)
);
}; };
certs.selfsigned = lib.mkOption { certs.selfsigned = lib.mkOption {
description = "Generate self-signed certificates signed by a Certificate Authority."; description = "Generate self-signed certificates signed by a Certificate Authority.";
default = {}; default = {};
type = lib.types.attrsOf ( type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
lib.types.submodule (
{ config, ... }:
{
options = { options = {
ca = lib.mkOption { ca = lib.mkOption {
type = lib.types.nullOr shb.contracts.ssl.cas; type = lib.types.nullOr contracts.ssl.cas;
description = '' description = ''
CA used to generate this certificate. Only used for self-signed. CA used to generate this certificate. Only used for self-signed.
@ -135,8 +106,8 @@ in
This option implements the SSL Generator contract. This option implements the SSL Generator contract.
''; '';
type = shb.contracts.ssl.certs-paths; type = contracts.ssl.certs-paths;
default = { default = rec {
key = "/var/lib/certs/selfsigned/${config._module.args.name}.key"; key = "/var/lib/certs/selfsigned/${config._module.args.name}.key";
cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert"; cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert";
}; };
@ -161,18 +132,13 @@ in
example = [ "nginx.service" ]; example = [ "nginx.service" ];
}; };
}; };
} }));
)
);
}; };
certs.letsencrypt = lib.mkOption { certs.letsencrypt = lib.mkOption {
description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/)."; description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
default = {}; default = {};
type = lib.types.attrsOf ( type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
lib.types.submodule (
{ config, ... }:
{
options = { options = {
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -203,7 +169,7 @@ in
This option implements the SSL Generator contract. This option implements the SSL Generator contract.
''; '';
type = shb.contracts.ssl.certs-paths; type = contracts.ssl.certs-paths;
default = { default = {
key = "/var/lib/acme/${config._module.args.name}/key.pem"; key = "/var/lib/acme/${config._module.args.name}/key.pem";
cert = "/var/lib/acme/${config._module.args.name}/cert.pem"; cert = "/var/lib/acme/${config._module.args.name}/cert.pem";
@ -229,17 +195,6 @@ in
default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service"; default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service";
}; };
afterAndWants = lib.mkOption {
description = ''
Systemd service(s) that must start successfully before attempting to reach acme.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.literalExpression ''
[ "dnsmasq.service" ]
'';
};
reloadServices = lib.mkOption { reloadServices = lib.mkOption {
description = '' description = ''
The list of systemd services to call `systemctl try-reload-or-restart` on. The list of systemd services to call `systemctl try-reload-or-restart` on.
@ -250,13 +205,7 @@ in
}; };
dnsProvider = lib.mkOption { dnsProvider = lib.mkOption {
description = '' description = "DNS provider to use. See https://go-acme.github.io/lego/dns/ for the list of supported providers.";
DNS provider to use.
See https://go-acme.github.io/lego/dns/ for the list of supported providers.
If null is given, use instead the reverse proxy to validate the domain.
'';
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "linode"; example = "linode";
@ -289,7 +238,6 @@ in
additionalEnvironment = lib.mkOption { additionalEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str; type = lib.types.attrsOf lib.types.str;
default = { };
description = '' description = ''
Additional environment variables used to configure the DNS provider. Additional environment variables used to configure the DNS provider.
@ -319,67 +267,26 @@ in
type = lib.types.str; type = lib.types.str;
}; };
stagingServer = lib.mkOption {
description = "User Let's Encrypt's staging server.";
type = lib.types.bool;
default = false;
};
debug = lib.mkOption { debug = lib.mkOption {
description = "Enable debug logging"; description = "Enable debug logging";
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
}; };
} }));
)
);
}; };
}; };
config = config =
let let
filterProvider = provider: lib.attrsets.filterAttrs (k: i: i.provider == provider);
serviceName = lib.strings.removeSuffix ".service"; serviceName = lib.strings.removeSuffix ".service";
in in
lib.mkMerge [ lib.mkMerge [
# Generic assertions
{
assertions = lib.flatten (
lib.mapAttrsToList (
_name: certCfg:
(
let
domainInExtraDomains =
(lib.lists.findFirstIndex (x: x == certCfg.domain) null certCfg.extraDomains) != null;
firstDuplicateDomain =
(
l:
let
sorted = lib.sort (x: y: x < y) l;
maybeDupe = lib.lists.removePrefix (lib.lists.commonPrefix (lib.uniqueStrings sorted) sorted) sorted;
in
if maybeDupe == [ ] then null else lib.head maybeDupe
)
certCfg.extraDomains;
in
[
{
assertion = !domainInExtraDomains;
message = "Error in SHB option for domain ${certCfg.domain}: do not repeat the domain name in the `extraDomain` option.";
}
{
assertion = firstDuplicateDomain == null;
message = "Error in SHB option for domain ${certCfg.domain}: `extraDomain` option cannot have duplicates, first offender is: ${firstDuplicateDomain} in the following list: ${lib.concatStringsSep " " certCfg.extraDomains}.";
}
]
)
) (cfg.certs.selfsigned // cfg.certs.letsencrypt)
);
}
# Config for self-signed CA. # Config for self-signed CA.
{ {
systemd.services = lib.mapAttrs' ( systemd.services = lib.mapAttrs' (_name: caCfg:
_name: caCfg:
lib.nameValuePair (serviceName caCfg.systemdService) { lib.nameValuePair (serviceName caCfg.systemdService) {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
wants = [ config.shb.certs.systemdService ]; wants = [ config.shb.certs.systemdService ];
@ -399,59 +306,48 @@ in
crl_signing_key crl_signing_key
EOF EOF
keyfile="${caCfg.paths.key}" mkdir -p "$(dirname -- "${caCfg.paths.key}")"
keydir="$(dirname -- "$keyfile")" ${pkgs.gnutls}/bin/certtool \
mkdir -p "$keydir"
[ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
--generate-privkey \ --generate-privkey \
--key-type rsa \ --key-type rsa \
--sec-param High \ --sec-param High \
--outfile "$keyfile" --outfile ${caCfg.paths.key}
chmod 666 "$keyfile" chmod 666 ${caCfg.paths.key}
certfile="${caCfg.paths.cert}" mkdir -p "$(dirname -- "${caCfg.paths.cert}")"
certdir="$(dirname -- "$certfile")" ${pkgs.gnutls}/bin/certtool \
mkdir -p "$certdir"
[ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
--generate-self-signed \ --generate-self-signed \
--load-privkey "$keyfile" \ --load-privkey ${caCfg.paths.key} \
--template ca.template \ --template ca.template \
--outfile "$certfile" --outfile ${caCfg.paths.cert}
chmod 666 "$certfile" chmod 666 ${caCfg.paths.cert}
''; '';
} }
) cfg.cas.selfsigned; ) cfg.cas.selfsigned;
} }
# Config for self-signed CA bundle. # Config for self-signed CA bundle.
{ {
systemd.services.${serviceName config.shb.certs.systemdService} = ( systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
lib.mkIf (cfg.cas.selfsigned != { }) {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
script = '' script = ''
mkdir -p /etc/ssl/certs mkdir -p /etc/ssl/certs
# This file is automatically idempotent since the source files used are idempotent.
rm -f /etc/ssl/certs/ca-bundle.crt rm -f /etc/ssl/certs/ca-bundle.crt
rm -f /etc/ssl/certs/ca-certificates.crt rm -f /etc/ssl/certs/ca-certificates.crt
cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-bundle.crt cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-bundle.crt
cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt
for file in ${ for file in ${lib.concatStringsSep " " (lib.mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)}; do
lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)
}; do
cat "$file" >> /etc/ssl/certs/ca-bundle.crt cat "$file" >> /etc/ssl/certs/ca-bundle.crt
cat "$file" >> /etc/ssl/certs/ca-certificates.crt cat "$file" >> /etc/ssl/certs/ca-certificates.crt
done done
''; '';
} });
);
} }
# Config for self-signed cert. # Config for self-signed cert.
{ {
systemd.services = lib.mapAttrs' ( systemd.services = lib.mapAttrs' (_name: certCfg:
_name: certCfg:
lib.nameValuePair (serviceName certCfg.systemdService) { lib.nameValuePair (serviceName certCfg.systemdService) {
after = [ certCfg.ca.systemdService ]; after = [ certCfg.ca.systemdService ];
requires = [ certCfg.ca.systemdService ]; requires = [ certCfg.ca.systemdService ];
@ -461,9 +357,10 @@ in
script = script =
let let
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
chmod = cert: '' chmod = cert:
chown root:${certCfg.group} "${cert}" ''
chmod 640 "${cert}" chown root:${certCfg.group} ${cert}
chmod 640 ${cert}
''; '';
in in
'' ''
@ -480,27 +377,23 @@ in
signing_key signing_key
EOF EOF
keyfile="${certCfg.paths.key}" mkdir -p "$(dirname -- "${certCfg.paths.key}")"
keydir="$(dirname -- "$keyfile")" ${pkgs.gnutls}/bin/certtool \
mkdir -p "$keydir"
[ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
--generate-privkey \ --generate-privkey \
--key-type rsa \ --key-type rsa \
--sec-param High \ --sec-param High \
--outfile "$keyfile" --outfile ${certCfg.paths.key}
${chmod "$keyfile"} ${chmod certCfg.paths.key}
certfile="${certCfg.paths.cert}" mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
certdir="$(dirname -- "$certfile")" ${pkgs.gnutls}/bin/certtool \
mkdir -p "$certdir"
[ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
--generate-certificate \ --generate-certificate \
--load-privkey "$keyfile" \ --load-privkey ${certCfg.paths.key} \
--load-ca-privkey "${certCfg.ca.paths.key}" \ --load-ca-privkey ${certCfg.ca.paths.key} \
--load-ca-certificate "${certCfg.ca.paths.cert}" \ --load-ca-certificate ${certCfg.ca.paths.cert} \
--template server.template \ --template server.template \
--outfile "$certfile" --outfile ${certCfg.paths.cert}
${chmod "$certfile"} ${chmod certCfg.paths.cert}
''; '';
postStart = lib.optionalString (certCfg.reloadServices != []) '' postStart = lib.optionalString (certCfg.reloadServices != []) ''
@ -514,172 +407,28 @@ in
} }
# Config for Let's Encrypt cert. # Config for Let's Encrypt cert.
{ {
users.users = lib.mkMerge ( users.users = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
mapAttrsToList (name: certCfg: {
${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [ ${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
config.security.acme.defaults.group config.security.acme.defaults.group
]; ];
}) cfg.certs.letsencrypt }) cfg.certs.letsencrypt);
);
security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true; security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true;
security.acme.certs = security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
let
extraDomainsCfg =
certCfg:
map (name: {
"${name}" = { "${name}" = {
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
email = certCfg.adminEmail; email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
};
}) certCfg.extraDomains;
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
[
{
"${name}" = {
extraDomainNames = certCfg.extraDomains;
email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
}
// lib.optionalAttrs (certCfg.dnsProvider != null) {
inherit (certCfg) dnsProvider dnsResolver; inherit (certCfg) dnsProvider dnsResolver;
inherit (certCfg) group reloadServices; inherit (certCfg) group reloadServices;
environmentFile = certCfg.credentialsFile; credentialsFile = certCfg.credentialsFile;
enableDebugLogs = certCfg.debug;
}; };
} }) cfg.certs.letsencrypt);
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
) cfg.certs.letsencrypt
)
);
services.nginx = systemd.services = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
let
extraDomainsCfg =
extraDomains:
map (name: {
virtualHosts."${name}" = {
# addSSL = true;
enableACME = true;
};
}) extraDomains;
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.dnsProvider == null) (
[
{
virtualHosts."${name}" = {
# addSSL = true;
enableACME = true;
};
}
]
++ extraDomainsCfg certCfg.extraDomains
)
) cfg.certs.letsencrypt
)
);
systemd.services =
let
extraDomainsCfg =
certCfg:
flatten (
map (
name:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${name}".environment = certCfg.additionalEnvironment;
}
]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${name}" = {
after = certCfg.afterAndWants;
wants = certCfg.afterAndWants;
};
}
]
) certCfg.extraDomains
);
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}".environment = certCfg.additionalEnvironment; "acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
} }) cfg.certs.letsencrypt);
]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}" = {
after = certCfg.afterAndWants;
wants = certCfg.afterAndWants;
};
}
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
) cfg.certs.letsencrypt
)
);
services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != { }) {
enable = true;
listenAddress = "127.0.0.1";
user = "acme";
paths =
let
pathCfg =
name: certCfg:
let
mainDomainPaths = map dirOf [
certCfg.paths.cert
certCfg.paths.key
];
# Not sure this will work for all cases.
mainPath = dirOf (dirOf certCfg.paths.cert);
extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains;
in
mainDomainPaths ++ extraDomainsPath;
in
unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt));
};
services.prometheus.scrapeConfigs =
let
scrapeCfg = name: certCfg: [
{
job_name = "node-cert-${name}";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = certCfg.domain;
};
} }
]; ];
} }
];
in
optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
}
(lib.mkIf (cfg.enableDashboard && (cfg.certs.selfsigned != { } || cfg.certs.letsencrypt != { })) {
shb.monitoring.dashboards = [
./ssl/dashboard/SSL.json
];
})
];
}

View file

@ -1,727 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 16,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line+area"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "transparent",
"value": 604808
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 3,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
"sortBy": "Last *",
"sortDesc": false
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"expr": "min by(exported_hostname, subject, path) (ssl_certificate_expiry_seconds{subject=~\"CN=$job\"})",
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
"range": true,
"refId": "A"
}
],
"title": "Certificate Remaining Validity",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": 0
}
]
},
"unit": "dateTimeFromNow"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 5,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true,
"sortBy": "Last *",
"sortDesc": true,
"width": 300
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"editorMode": "code",
"exemplar": false,
"expr": "systemd_timer_next_trigger_seconds{name=~\"acme-renew-$job.timer\"} * 1000",
"format": "time_series",
"instant": false,
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Schedule",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "green",
"mode": "fixed"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"footer": {
"reducers": []
},
"inspect": false
},
"decimals": 0,
"mappings": [],
"noValue": "0",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "green",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "% failed"
},
"properties": [
{
"id": "unit",
"value": "percentunit"
},
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "color-background"
}
},
{
"id": "color",
"value": {
"mode": "continuous-GrYlRd"
}
},
{
"id": "max",
"value": 1
}
]
},
{
"matcher": {
"id": "byName",
"options": "total"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "color-background"
}
},
{
"id": "color",
"value": {
"mode": "thresholds"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "transparent",
"value": 1
}
]
}
}
]
},
{
"matcher": {
"id": "byType",
"options": "string"
},
"properties": [
{
"id": "custom.minWidth",
"value": 150
}
]
},
{
"matcher": {
"id": "byType",
"options": "number"
},
"properties": [
{
"id": "custom.width",
"value": 100
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 4,
"options": {
"cellHeight": "sm",
"enablePagination": true,
"frozenColumns": {},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"exemplar": false,
"expr": "increase(systemd_unit_state{name=~\"acme-(order-renew-)?[[job]].service\", state=\"activating\"}[7d])",
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"exemplar": false,
"expr": "increase(systemd_unit_state{name=~\"acme-(order-renew-)?[[job]].service\", state=\"failed\"}[7d])",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "B"
}
],
"title": "Jobs in the Past Week",
"transformations": [
{
"id": "labelsToFields",
"options": {
"mode": "columns"
}
},
{
"id": "merge",
"options": {}
},
{
"id": "groupingToMatrix",
"options": {
"columnField": "state",
"rowField": "name",
"valueField": "Value"
}
},
{
"id": "calculateField",
"options": {
"alias": "total",
"binary": {
"left": {
"matcher": {
"id": "byName",
"options": "activating"
}
},
"operator": "+",
"right": {
"matcher": {
"id": "byName",
"options": "failed"
}
}
},
"mode": "binary",
"reduce": {
"include": [
"activating",
"failed"
],
"reducer": "sum"
}
}
},
{
"id": "calculateField",
"options": {
"alias": "% failed",
"binary": {
"left": {
"matcher": {
"id": "byName",
"options": "failed"
}
},
"operator": "/",
"right": {
"matcher": {
"id": "byName",
"options": "total"
}
}
},
"mode": "binary",
"reduce": {
"reducer": "sum"
}
}
},
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"desc": true,
"field": "total"
}
]
}
},
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"desc": true,
"field": "failed"
}
]
}
},
{
"id": "organize",
"options": {
"excludeByName": {},
"includeByName": {},
"indexByName": {},
"renameByName": {
"activating": "success",
"name\\state": "Job"
}
}
}
],
"type": "table"
},
{
"description": "",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"gridPos": {
"h": 2,
"w": 12,
"x": 12,
"y": 8
},
"id": 7,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "If the log panel is empty, it may be because the amount of lines is too high. Try filtering a few jobs first.",
"mode": "markdown"
},
"pluginVersion": "12.2.0",
"title": "",
"type": "text"
},
{
"datasource": {
"default": false,
"type": "loki",
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
},
"fieldConfig": {
"defaults": {},
"overrides": []
},
"gridPos": {
"h": 14,
"w": 12,
"x": 12,
"y": 10
},
"id": 8,
"options": {
"dedupStrategy": "none",
"enableInfiniteScrolling": false,
"enableLogDetails": false,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "loki",
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
},
"direction": "backward",
"editorMode": "code",
"expr": "{unit=~\"acme-(order-renew-)?($job).service\"}",
"queryType": "range",
"refId": "A"
}
],
"title": "Logs - $job",
"type": "logs"
},
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"description": "The job duration is not accurate. Jobs taking less than 15s to run will sometimes appear as taking 100s.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "fixed"
},
"custom": {
"axisPlacement": "auto",
"fillOpacity": 70,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineWidth": 0,
"spanNulls": false
},
"mappings": [
{
"options": {
"1": {
"color": "green",
"index": 0,
"text": "Running"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "#EAB839",
"value": 0
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 6,
"options": {
"alignValue": "left",
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"mergeValues": true,
"rowHeight": 0.9,
"showValue": "never",
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
},
"editorMode": "code",
"exemplar": false,
"expr": "max(label_replace(systemd_unit_state{name=~\"acme-(order-renew-)?$job.service\", state=\"activating\"}, \"name\", \"$1\", \"name\", \"acme-order-renew-(.*).service\") > 0 or on(name) label_replace(clamp(systemd_timer_last_trigger_seconds{name=~\"acme-renew-$job.timer\"} - (systemd_timer_last_trigger_seconds{name=~\"acme-renew-$job.timer\"} offset 100s) > 0, 0, 1), \"name\", \"$1\", \"name\", \"acme-renew-(.*).timer\")) by (name)",
"format": "time_series",
"hide": false,
"instant": false,
"key": "Q-e1d5c07a-8dcc-4f34-aa5c-cdebcbdda322-0",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Job Runs",
"type": "state-timeline"
}
],
"preload": false,
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": [
"All"
],
"value": [
"$__all"
]
},
"definition": "label_values(systemd_unit_state{name=~\"acme-renew-.*.timer\"},name)",
"includeAll": true,
"label": "Job",
"multi": true,
"name": "job",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(systemd_unit_state{name=~\"acme-renew-.*.timer\"},name)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "/acme-renew-(?<value>.*).timer/",
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "SSL Certificates",
"uid": "ae818js0bvw8wb",
"version": 25
}

View file

@ -1,4 +1,4 @@
# SSL Generator Block {#block-ssl} # SSL Generator Block {#ssl-block}
This NixOS module is a block that implements the [SSL certificate generator](contracts-ssl.html) contract. This NixOS module is a block that implements the [SSL certificate generator](contracts-ssl.html) contract.
@ -14,7 +14,7 @@ It is implemented by:
[11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned [11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned
[12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt [12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt
## Self-Signed Certificates {#block-ssl-impl-self-signed} ## Self-Signed Certificates {#ssl-block-impl-self-signed}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix). Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
@ -52,7 +52,7 @@ shb.certs.certs.selfsigned = {
The group has been chosen to be `nginx` to be consistent with the examples further down in this The group has been chosen to be `nginx` to be consistent with the examples further down in this
document. document.
## Let's Encrypt {#block-ssl-impl-lets-encrypt} ## Let's Encrypt {#ssl-block-impl-lets-encrypt}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix). Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
@ -62,7 +62,6 @@ We can ask Let's Encrypt to generate a certificate with:
shb.certs.certs.letsencrypt."example.com" = { shb.certs.certs.letsencrypt."example.com" = {
domain = "example.com"; domain = "example.com";
group = "nginx"; group = "nginx";
reloadServices = [ "nginx.service" ];
dnsProvider = "linode"; dnsProvider = "linode";
adminEmail = "admin@example.com"; adminEmail = "admin@example.com";
credentialsFile = /path/to/secret/file; credentialsFile = /path/to/secret/file;
@ -80,16 +79,9 @@ The credential file's content would be a key-value pair:
LINODE_TOKEN=XYZ... LINODE_TOKEN=XYZ...
``` ```
If you use one subdomain per service,
asking for certificates for a subdomain is done with:
```nix
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "nextcloud.${domain}" ];
```
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/). For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
## Usage {#block-ssl-usage} ## Usage {#ssl-block-usage}
To use either a self-signed certificates or a Let's Encrypt generated one, we can reference the path To use either a self-signed certificates or a Let's Encrypt generated one, we can reference the path
where the certificate and the private key are located: where the certificate and the private key are located:
@ -118,40 +110,16 @@ config.shb.certs.systemdService
See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage
example. example.
## Monitoring {#blocks-ssl-monitoring} ## Debug {#ssl-block-debug}
A dashboard for SSL certificates is provided.
See [SSL Certificates Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-ssl) section in the monitoring chapter.
## Debug {#block-ssl-debug}
Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService` Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService`
option. You can then see the latest errors messages using `journalctl`. option. You can then see the latest errors messages using `journalctl`.
### Let's Encrypt debug {#blocks-ssl-debug-lets-encrypt} ## Tests {#ssl-block-tests}
Since the SHB SSL block uses the [`security.acme`][] module under the hood,
knowing how that one works can become required if something goes wrong.
For each domain and subdomain, noted as `fqdn` hereunder,
the following systemd timers and services are created:
- `acme-renew-${fqdn}.timer` triggers the `acme-order-renew-${fqdn}.service` service every day.
- `acme-${fqdn}.service` (re)generate the initial self-signed certificate,
only if the following job never succeeded at least once yet.
- `acme-order-renew-${fqdn}.service` asks for a new certificate
only if the certificate will expire in the next 30 days.
Has logic to only renew if the list of domains has not changed.
Also, a global service named `acme-setup.service` is created
[`security.acme`]: https://nixos.org/manual/nixos/stable/#module-security-acme
## Tests {#block-ssl-tests}
The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix). The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
## Options Reference {#block-ssl-options} ## Options Reference {#ssl-block-options}
```{=include=} options ```{=include=} options
id-prefix: blocks-ssl-options- id-prefix: blocks-ssl-options-

View file

@ -1,43 +1,32 @@
# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances. # Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances.
{ { config, lib, pkgs, ... }:
config,
lib,
pkgs,
...
}:
with lib; with lib;
let let
cfg = config.shb.tinyproxy; cfg = config.shb.tinyproxy;
mkValueStringTinyproxy = mkValueStringTinyproxy = with lib; v:
with lib; if true == v then "yes"
v: else if false == v then "no"
if true == v then else generators.mkValueStringDefault {} v;
"yes"
else if false == v then
"no"
else
generators.mkValueStringDefault { } v;
mkKeyValueTinyproxy = mkKeyValueTinyproxy = {
{ mkValueString ? mkValueStringDefault {}
mkValueString ? mkValueStringDefault { }, }: sep: k: v:
}: if null == v then ""
sep: k: v: else "${lib.strings.escape [sep] k}${sep}${mkValueString v}";
if null == v then "" else "${lib.strings.escape [ sep ] k}${sep}${mkValueString v}";
settingsFormat = ( settingsFormat = (pkgs.formats.keyValue {
pkgs.formats.keyValue {
mkKeyValue = mkKeyValueTinyproxy { mkKeyValue = mkKeyValueTinyproxy {
mkValueString = mkValueStringTinyproxy; mkValueString = mkValueStringTinyproxy;
} " "; } " ";
listsAsDuplicateKeys= true; listsAsDuplicateKeys= true;
} });
);
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings; configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
someEnabled = any (mapAttrsToList (name: c: c.enable) cfg);
in in
{ {
options = options =
@ -46,7 +35,7 @@ in
options = { options = {
enable = mkEnableOption "Tinyproxy daemon"; enable = mkEnableOption "Tinyproxy daemon";
package = mkPackageOption pkgs "tinyproxy" { }; package = mkPackageOptionMD pkgs "tinyproxy" {};
dynamicBindFile = mkOption { dynamicBindFile = mkOption {
description = '' description = ''
@ -60,8 +49,7 @@ in
Configuration for [tinyproxy](https://tinyproxy.github.io/). Configuration for [tinyproxy](https://tinyproxy.github.io/).
''; '';
default = { }; default = { };
example = literalExpression '' example = literalExpression ''{
{
Port 8888; Port 8888;
Listen 127.0.0.1; Listen 127.0.0.1;
Timeout 600; Timeout 600;
@ -69,9 +57,7 @@ in
Anonymous = ['"Host"' '"Authorization"']; Anonymous = ['"Host"' '"Authorization"'];
ReversePath = '"/example/" "http://www.example.com/"'; ReversePath = '"/example/" "http://www.example.com/"';
}''; }'';
type = types.submodule ( type = types.submodule ({name, ...}: {
{ name, ... }:
{
freeformType = settingsFormat.type; freeformType = settingsFormat.type;
options = { options = {
Listen = mkOption { Listen = mkOption {
@ -107,8 +93,7 @@ in
''; '';
}; };
}; };
} });
);
}; };
}; };
}; };
@ -124,9 +109,7 @@ in
config = { config = {
systemd.services = systemd.services =
let let
instanceConfig = instanceConfig = name: c: mkIf c.enable {
name: c:
mkIf c.enable {
"tinyproxy-${name}" = { "tinyproxy-${name}" = {
description = "TinyProxy daemon - instance ${name}"; description = "TinyProxy daemon - instance ${name}";
after = [ "network.target" ]; after = [ "network.target" ];
@ -145,15 +128,12 @@ in
RestartMaxDelaySec = "10s"; RestartMaxDelaySec = "10s";
ConfigurationDirectory = "tinyproxy"; ConfigurationDirectory = "tinyproxy";
}; };
preStart = concatStringsSep "\n" ( preStart = concatStringsSep "\n" ([
[
"cat ${configFile name c} > /etc/tinyproxy/${name}.conf" "cat ${configFile name c} > /etc/tinyproxy/${name}.conf"
] ] ++ optionals (c.dynamicBindFile != "") [
++ optionals (c.dynamicBindFile != "") [
"echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf" "echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf"
"cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf" "cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf"
] ]);
);
}; };
}; };
in in

View file

@ -1,9 +1,4 @@
{ { config, pkgs, lib, ... }:
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.vpn; cfg = config.shb.vpn;
@ -11,14 +6,12 @@ let
quoteEach = lib.concatMapStrings (x: ''"${x}"''); quoteEach = lib.concatMapStrings (x: ''"${x}"'');
nordvpnConfig = nordvpnConfig =
{ { name
name, , dev
dev, , authFile
authFile, , remoteServerIP
remoteServerIP, , dependentServices ? []
dependentServices ? [ ], }: ''
}:
''
client client
dev ${dev} dev ${dev}
proto tcp proto tcp
@ -110,16 +103,10 @@ let
</tls-auth> </tls-auth>
''; '';
routeUp = routeUp = name: dependentServices: pkgs.writeShellApplication {
name: dependentServices:
pkgs.writeShellApplication {
name = "routeUp.sh"; name = "routeUp.sh";
runtimeInputs = [ runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools ];
pkgs.iproute2
pkgs.systemd
pkgs.nettools
];
text = '' text = ''
echo "Running route-up..." echo "Running route-up..."
@ -163,17 +150,10 @@ let
''; '';
}; };
routeDown = routeDown = name: dependentServices: pkgs.writeShellApplication {
name: dependentServices:
pkgs.writeShellApplication {
name = "routeDown.sh"; name = "routeDown.sh";
runtimeInputs = [ runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools pkgs.coreutils ];
pkgs.iproute2
pkgs.systemd
pkgs.nettools
pkgs.coreutils
];
text = '' text = ''
echo "Running route-down..." echo "Running route-down..."
@ -217,6 +197,8 @@ let
echo "Running route-down DONE" echo "Running route-down DONE"
''; '';
}; };
someEnabled = lib.any (lib.mapAttrsToList (name: c: c.enable) cfg);
in in
{ {
options = options =
@ -225,7 +207,7 @@ in
options = { options = {
enable = lib.mkEnableOption "OpenVPN config"; enable = lib.mkEnableOption "OpenVPN config";
package = lib.mkPackageOption pkgs "openvpn" { }; package = lib.mkPackageOptionMD pkgs "openvpn" {};
provider = lib.mkOption { provider = lib.mkOption {
description = "VPN provider, if given uses ready-made configuration."; description = "VPN provider, if given uses ready-made configuration.";
@ -274,9 +256,7 @@ in
config = { config = {
services.openvpn.servers = services.openvpn.servers =
let let
instanceConfig = instanceConfig = name: c: lib.mkIf c.enable {
name: c:
lib.mkIf c.enable {
${name} = { ${name} = {
autoStart = true; autoStart = true;
@ -292,20 +272,17 @@ in
in in
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
systemd.tmpfiles.rules = map (name: "d /tmp/openvpn/${name}.status 0700 root root") ( systemd.tmpfiles.rules = map (name:
lib.attrNames cfg "d /tmp/openvpn/${name}.status 0700 root root"
); ) (lib.attrNames cfg);
networking.iproute2.enable = true; networking.iproute2.enable = true;
networking.iproute2.rttablesExtraConfig = lib.concatStringsSep "\n" ( networking.iproute2.rttablesExtraConfig =
lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg);
);
shb.tinyproxy = shb.tinyproxy =
let let
instanceConfig = instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) {
name: c:
lib.mkIf (c.enable && c.proxyPort != null) {
${name} = { ${name} = {
enable = true; enable = true;
# package = pkgs.tinyproxy.overrideAttrs (old: { # package = pkgs.tinyproxy.overrideAttrs (old: {
@ -324,10 +301,7 @@ in
Listen = "127.0.0.1"; Listen = "127.0.0.1";
Syslog = "On"; Syslog = "On";
LogLevel = "Info"; LogLevel = "Info";
Allow = [ Allow = [ "127.0.0.1" "::1" ];
"127.0.0.1"
"::1"
];
ViaProxyName = ''"tinyproxy"''; ViaProxyName = ''"tinyproxy"'';
}; };
}; };

View file

@ -1,259 +0,0 @@
{
config,
pkgs,
lib,
shb,
utils,
...
}:
let
cfg = config.shb.zfs;
datasets =
if cfg.snapshotBeforeActivation.datasets != null then
cfg.snapshotBeforeActivation.datasets
else
config.boot.zfs.extraPools;
in
{
imports = [
../../lib/module.nix
];
options.shb.zfs = {
pools = lib.mkOption {
description = ''
Attrset of ZFS pools under which datasets will be created.
The ZFS pools are not managed by this module, they should already exist.
Each pool named here will be added to the [`boot.zfs.extraPools`](https://search.nixos.org/options?channel=unstable&include_modular_service_options=0&include_nixos_options=1&query=boot.zfs.extrapools&show=option:boot.zfs.extraPools) option.
'';
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options = {
datasets = lib.mkOption {
description = ''
ZFS Datasets.
Each entry in the attrset will be created and mounted in the given path.
The attrset name is the dataset name.
This block implements the following contracts:
- mount
'';
default = { };
example = lib.literalExpression ''
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
'';
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
options = {
enable = lib.mkEnableOption "shb.zfs.datasets";
path = lib.mkOption {
type = lib.types.str;
description = "Path this dataset should be mounted on. If the string 'none' is given, the dataset will not be mounted.";
};
mode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "If non null, unix mode to apply to the dataset root folder.";
default = null;
example = "ug=rwx,g+s";
};
owner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "If non null, unix user to apply to the dataset root folder.";
default = null;
example = "syncthing";
};
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "If non null, unix group to apply to the dataset root folder.";
default = null;
example = "syncthing";
};
defaultACLs = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
If non null, default ACL to set on the dataset root folder.
Executes "setfacl -d -m $acl $path"
'';
default = null;
example = "g:syncthing:rwX";
};
after = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
Order creating this dataset after the mentioned ones.
This only works with datasets managed by this module.
Use the name of the dataset without the pool name.
'';
default = [ ];
example = lib.literalExpression ''
[
"backup"
]
'';
};
backup = lib.mkOption {
description = ''
Backup contract consumer configuration.
This contract will backup the files inside the dataset.
'';
type = lib.types.submodule {
options = shb.contracts.backup.mkRequester {
user = if config.owner == null then "root" else config.owner;
sourceDirectories = [
config.path
];
sourceDirectoriesText = ''
[
shb.zfs.pools.<name>.datasets.<name>.path
]
'';
};
};
};
datasetbackup = lib.mkOption {
description = ''
ZFS dataset backup contract configuration.
This contract will take snaphots of the dataset.
'';
type = lib.types.submodule {
options = shb.contracts.datasetbackup.mkRequester {
dataset = name;
};
};
};
};
}
)
);
};
};
}
);
};
snapshotBeforeActivation = {
enable = lib.mkOption {
type = lib.types.bool;
description = "Take a snapshot of all datasets before activation";
default = false;
};
template = lib.mkOption {
type = lib.types.str;
description = "Bash command to generate the snapshot name. $1 is the path to the new generation.";
default = ''pre-$(date --utc '+%y%m%dT%H%M%S')-$(basename "$1")'';
};
datasets = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
description = ''
Defines all datasets to take a snapshot of.
If set to `null`, the default, take the list of datasets from `config.boot.zfs.extraPools`.
The snapshots are not recursive unless specificed in the `recursive` option.
'';
};
recursive = lib.mkOption {
type = lib.types.bool;
description = "If true, take snapshots recurisevly on the given datasets.";
default = false;
};
};
};
# The implementation is greatly inspired by https://discourse.nixos.org/t/configure-zfs-filesystems-after-install/48633/3
config = {
boot.zfs.extraPools = lib.uniqueStrings (builtins.attrNames cfg.pools);
systemd.services =
let
mkPool =
poolName: poolCfg: lib.listToAttrs (lib.mapAttrsToList (mkDataset poolName) poolCfg.datasets);
mkDataset =
poolName: name: cfg':
let
dataset = poolName + "/" + name;
in
lib.attrsets.nameValuePair "zfs-create-${utils.escapeSystemdPath dataset}" {
# oneshot is used to make the systemd service wait on completion of the script.
serviceConfig.Type = "oneshot";
unitConfig.DefaultDependencies = false;
requiredBy = [ "local-fs.target" ];
before = [ "local-fs.target" ];
after = [
"zfs-import-${poolName}.service"
"zfs-mount.service"
]
++ map (n: "zfs-create-${poolName}-${n}.service") cfg'.after;
unitConfig.ConditionPathIsMountPoint = lib.mkIf (cfg'.path != "none") "!${cfg'.path}";
script = ''
${pkgs.zfs}/bin/zfs list ${dataset} > /dev/null 2>&1 \
|| ${pkgs.zfs}/bin/zfs create \
-o mountpoint=none \
${dataset} || :
[ "$(${pkgs.zfs}/bin/zfs get -H mountpoint -o value ${dataset})" = ${cfg'.path} ] \
|| ${pkgs.zfs}/bin/zfs set \
mountpoint="${cfg'.path}" \
${dataset}
''
+ lib.optionalString (cfg'.path != "none" && cfg'.mode != null) ''
chmod "${cfg'.mode}" "${cfg'.path}"
''
+ lib.optionalString (cfg'.path != "none" && cfg'.owner != null) ''
chown "${cfg'.owner}" "${cfg'.path}"
''
+ lib.optionalString (cfg'.path != "none" && cfg'.group != null) ''
chown :"${cfg'.group}" "${cfg'.path}"
''
+ lib.optionalString (cfg'.path != "none" && cfg'.defaultACLs != null) ''
${pkgs.acl}/bin/setfacl -d -m "${cfg'.defaultACLs}" "${cfg'.path}"
'';
};
mergeAttrs = lib.foldl lib.mergeAttrs { };
in
mergeAttrs (lib.mapAttrsToList mkPool cfg.pools);
system.preSwitchChecks."shb-zfs-snapshot" = lib.mkIf cfg.snapshotBeforeActivation.enable (
''
name="${cfg.snapshotBeforeActivation.template}"
''
+ (
let
recursiveFlag = lib.optionalString cfg.snapshotBeforeActivation.recursive "-r";
in
lib.concatMapStringsSep "\n" (ds: ''
echo "Taking ZFS snapshot of ${ds}@$name"
zfs snapshot ${recursiveFlag} ${ds}@"$name"
'') (lib.uniqueStrings datasets)
)
);
};
}

View file

@ -1,138 +0,0 @@
# ZFS Block {#blocks-zfs}
Defined in [`/modules/blocks/zfs.nix`](@REPO@/modules/blocks/zfs.nix):
```nix
{
inputs,
...
}:
{
imports = [
inputs.selfhostblocks.nixosModules.zfs
];
}
```
This block creates ZFS datasets, optionally mounts them and sets permissions on the mount point.
It also enables taking snapshots on activation.
## Features {#blocks-zfs-features}
- Creates ZFS dataset which is [optionally mounted](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path).
- Sets permissions, [owner](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.owner), [group](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.group) and [ACL](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.defaultACLs) on the mount point.
- Backup of the files in the dataset [`shb.zfs.<name>.backup`][backup] through the [backup contract](./contracts-backup.html).
- Backup of the dataset itself [`shb.zfs.<name>.datasetbackup`][datasetbackup] through the [dataset backup contract](./contracts-datasetbackup.html).
- Take a snapshot [before activating or deploying][activationsnapshot] allowing a safe rollback.
[backup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.backup
[datasetbackup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup
[activationsnapshot]: #blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable
## Usage {#blocks-zfs-usage}
Create a dataset at `root/safe/users` mounted on `/var/lib/nixos`:
```nix
shb.zfs.pools.root.datasets."safe/users".path = "/var/lib/nixos";
```
Create a dataset at `backup/syncoid` but do not mount it:
```nix
shb.zfs.pools.backup.datasets."syncoid".path = "none";
```
Create a dataset at `root/syncthing` and set custom permissions and ACL.
Permission and ACL are only enforced for the mount point.
```nix
shb.zfs.pools.root.datasets."syncthing" = {
path = "/srv/syncthing";
mode = "ug=rwx,g+s,o=";
owner = "syncthing";
group = "syncthing";
defaultACLs = "g:syncthing:rwX";
};
```
### Backup dataset {#blocks-zfs-usage-backup-dataset}
To backup the dataset directly, use the [dataset backup contract](contracts-datasetbackup.html).
For example, with the sanoid module as the dataset backup contract provider:
```nix
{
shb.zfs.pools.root.datasets.home = {
path = "/home";
};
shb.sanoid.backup."root/home" = {
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
template = "yearly";
};
}
```
See the [Sanoid block](blocks-sanoid.html) for more examples.
### Backup files {#blocks-zfs-usage-backup-files}
To backup the files in the dataset, use the [file backup contract](contracts-backup.html).
For example, with the restic module as the file backup contract provider:
```nix
{
shb.zfs.pools.root.datasets.home = {
path = "/home";
};
shb.restic.instances."myservice" = {
request = shb.zfs.pools.root.datasets.home.backup.request;
};
}
```
See the [Restic block](blocks-restic.html) for more examples.
### Snapshot before activation {#blocks-zfs-usage-snapshot-before-activation}
To take a snapshot of all datasets before activating a new configuration:
```nix
{
shb.zfs.snapshotBeforeActivation.enable = true;
}
```
This does not take a recursive snapshot by default, to do it recursively, do:
```nix
{
shb.zfs.snapshotBeforeActivation = {
enable = true;
recursive = true;
};
}
```
We did not specify datasets manually, which means the datasets list comes from `boot.zfs.extraPools`.
To specify the datasets manually, you can use:
```nix
{
shb.zfs.snapshotBeforeActivation = {
enable = true;
datasets = [ "root" "root/var" ];
};
}
```
## Options Reference {#blocks-zfs-options}
```{=include=} options
id-prefix: blocks-zfs-options-
list-id: selfhostblocks-block-zfs-options
source: @OPTIONS_JSON@
```

View file

@ -1,284 +0,0 @@
{
lib,
shb,
pkgs,
...
}:
let
inherit (lib)
concatStringsSep
literalMD
mkOption
optionalAttrs
optionalString
;
inherit (lib.types)
listOf
nonEmptyListOf
submodule
str
;
inherit (shb) anyNotNull;
in
{
mkRequest =
{
user ? "",
userText ? null,
sourceDirectories ? [ "/var/lib/example" ],
sourceDirectoriesText ? null,
excludePatterns ? [ ],
excludePatternsText ? null,
beforeBackup ? [ ],
beforeBackupText ? null,
afterBackup ? [ ],
afterBackupText ? null,
}:
mkOption {
description = ''
Request part of the backup contract.
Options set by the requester module
enforcing how to backup files.
'';
default = {
inherit user sourceDirectories excludePatterns;
hooks = {
inherit beforeBackup afterBackup;
};
};
defaultText =
optionalString
(anyNotNull [
userText
sourceDirectoriesText
excludePatternsText
beforeBackupText
afterBackupText
])
(literalMD ''
{
user = ${if userText != null then userText else user};
sourceDirectories = ${
if sourceDirectoriesText != null then
sourceDirectoriesText
else
"[ " + concatStringsSep " " sourceDirectories + " ]"
};
excludePatterns = ${
if excludePatternsText != null then
excludePatternsText
else
"[ " + concatStringsSep " " excludePatterns + " ]"
};
hooks.beforeBackup = ${
if beforeBackupText != null then
beforeBackupText
else
"[ " + concatStringsSep " " beforeBackup + " ]"
};
hooks.afterBackup = ${
if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"
};
};
'');
type = submodule {
options = {
user =
mkOption {
description = ''
Unix user doing the backups.
'';
type = str;
example = "vaultwarden";
default = user;
}
// optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
sourceDirectories =
mkOption {
description = "Directories to backup.";
type = nonEmptyListOf str;
example = "/var/lib/vaultwarden";
default = sourceDirectories;
}
// optionalAttrs (sourceDirectoriesText != null) {
defaultText = literalMD sourceDirectoriesText;
};
excludePatterns =
mkOption {
description = "File patterns to exclude.";
type = listOf str;
default = excludePatterns;
}
// optionalAttrs (excludePatternsText != null) {
defaultText = literalMD excludePatternsText;
};
hooks = mkOption {
description = "Hooks to run around the backup.";
default = { };
type = submodule {
options = {
beforeBackup =
mkOption {
description = "Hooks to run before backup.";
type = listOf str;
default = beforeBackup;
}
// optionalAttrs (beforeBackupText != null) {
defaultText = literalMD beforeBackupText;
};
afterBackup =
mkOption {
description = "Hooks to run after backup.";
type = listOf str;
default = afterBackup;
}
// optionalAttrs (afterBackupText != null) {
defaultText = literalMD afterBackupText;
};
};
};
};
};
};
};
mkResult =
{
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}:
mkOption {
description = ''
Result part of the backup contract.
Options set by the provider module that indicates the name of the backup and restore scripts.
'';
default = {
inherit restoreScript backupService;
};
defaultText =
optionalString
(anyNotNull [
restoreScriptText
backupServiceText
])
(literalMD ''
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
}
'');
type = submodule {
options = {
restoreScript =
mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
<snapshot 1> <metadata>
<snapshot 2> <metadata>
```
And restore the database with:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore <snapshot 1>
```
'';
type = str;
default = restoreScript;
}
// optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
backupService =
mkOption {
description = ''
Name of service backing up the database.
This script can be ran manually to backup the database:
```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
```
'';
type = str;
default = backupService;
}
// optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
};
};
};
};
passthru.mkRestoreScript =
{
name,
user,
sudoPreserveEnvs ? [ ],
secretsFile,
restoreCmd,
listCmd,
}:
pkgs.writeShellApplication {
inherit name;
text = ''
usage() {
echo "$0 snapshots"
echo "$0 restore <snapshot>"
}
sudocmd() {
sudo --preserve-env=${lib.concatStringsSep "," sudoPreserveEnvs} -u ${user} "$@"
}
loadenv() {
set -a
# shellcheck disable=SC1090
source <(sudocmd cat "${secretsFile}")
set +a
}
if [ "$1" = "restore" ]; then
shift
snapshot="$1"
loadenv
echo "Will restore snapshot '$snapshot'"
sudocmd sh -c "${restoreCmd}"
elif [ "$1" = "snapshots" ]; then
shift
loadenv
sudocmd sh -c "${listCmd}"
else
usage
exit 1
fi
'';
};
}

View file

@ -1,104 +0,0 @@
# Backup Contract {#contract-backup}
This NixOS contract represents a backup job
that will backup one or more files or directories
on a regular schedule.
It is a contract between a service that has files to be backed up
and a service that backs up files.
## Contract Reference {#contract-backup-options}
These are all the options that are expected to exist for this contract to be respected.
```{=include=} options
id-prefix: contracts-backup-options-
list-id: selfhostblocks-options
source: @OPTIONS_JSON@
```
## Usage {#contract-backup-usage}
A service that can be backed up will provide a `backup` option.
Here is an example module defining such a `backup` option,
which defines what directories to backup (`sourceDirectories`)
and the user to backup with (`user`).
```nix
{
options = {
myservice.backup = mkOption {
type = lib.types.submodule {
options = shb.contracts.backup.mkRequester {
user = "nextcloud";
sourceDirectories = [
"/var/lib/nextcloud"
];
};
};
};
};
};
```
Now, on the other side we have a service that uses this `backup` option and actually backs up files.
This service is a `provider` of this contract and will provide a `result` option.
Let's assume such a module is available under the `backupService` option
and that one can create multiple backup instances under `backupService.instances`.
Then, to actually backup the `myservice` service, one would write:
```nix
backupService.instances.myservice = {
request = myservice.backup.request;
settings = {
enable = true;
repository = {
path = "/srv/backup/myservice";
};
# ... Other options specific to backupService like scheduling.
};
};
```
It is advised to backup files to different location, to improve redundancy.
Thanks to using contracts, this can be made easily either with the same `backupService`:
```nix
backupService.instances.myservice_2 = {
request = myservice.backup.request;
settings = {
enable = true;
repository = {
path = "<remote path>";
};
};
};
```
Or with another module `backupService_2`!
## Providers of the Backup Contract {#contract-backup-providers}
- [Restic block](blocks-restic.html).
- [Borgbackup block](blocks-borgbackup.html).
- [ZFS block](blocks-zfs.html).
## Requester Blocks and Services {#contract-backup-requesters}
- <!-- [ -->Audiobookshelf<!-- ](services-audiobookshelf.html). --> (no manual yet)
- <!-- [ -->Deluge<!--](services-deluge.html). --> (no manual yet)
- <!-- [ -->Grocy<!--](services-grocy.html). --> (no manual yet)
- <!-- [ -->Hledger<!--](services-hledger.html). --> (no manual yet)
- <!-- [ -->Home Assistant<!--](services-home-assistant.html). --> (no manual yet)
- <!-- [ -->Jellyfin<!--](services-jellyfin.html). --> (no manual yet)
- <!-- [ -->LLDAP<!--](blocks-ldap.html). --> (no manual yet)
- [Nextcloud](services-nextcloud.html#services-nextcloudserver-usage-backup).
- [Vaultwarden](services-vaultwarden.html#services-vaultwarden-backup).
- <!-- [ -->*arr<!--](services-arr.html). --> (no manual yet)

Some files were not shown because too many files have changed in this diff Show more