Skip to content

Use on-demand activation for low-traffic services - #688

Merged
attilaolah merged 9 commits into
mainfrom
socket-activate
May 11, 2026
Merged

Use on-demand activation for low-traffic services#688
attilaolah merged 9 commits into
mainfrom
socket-activate

Conversation

@attilaolah

@attilaolah attilaolah commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Documentation

    • Clarified ACME deployment steps: deployment now mentions installation of the proxy socket and socket-activated on-demand ACME services.
  • New Features

    • ACME now starts on-demand via socket activation.
    • OpenSSH can be started on-demand.
    • Encrypted ACME database mounting is initialized and validated automatically at boot.
  • Refactor

    • ACME backend reworked to use a local proxy and localhost binding for better isolation.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7efdaea6-a072-4132-97a7-39e33d03a236

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0b291 and 1504707.

📒 Files selected for processing (1)
  • modules/tags/acme.nix
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/tags/acme.nix

📝 Walkthrough

Walkthrough

Refactors the ACME backend: local backend bound to 127.0.0.1:port+1, socket-activated systemd proxy exposes the external ACME port, gocryptfs mounts a TPM-sealed encrypted Badger DB declaratively, and preparatory/permission rules and packages are added.

Changes

ACME Backend and Encryption Infrastructure

Layer / File(s) Summary
DB Path and Extpass Helpers
modules/tags/acme.nix
Introduces backendAddress, refactors dbCryptPath, and adds dbExtpass helper for TPM-sealed key unsealing via gocryptfs extpass.
Step CA Configuration
modules/tags/acme.nix
Step CA JSON binds backend to 127.0.0.1:${acme.port + 1} and sets Badger db.dataSource to ${dbPath} with db.type = badgerv2.
Tmpfiles: Ensure Paths
modules/tags/acme.nix
Ensures dbCryptPath and dbPath exist with expected permissions via systemd.tmpfiles.rules.
DB Preparation Service
modules/tags/acme.nix
Replaces mount oneshot with step-ca-db-prepare oneshot that validates the sealed key and initializes gocryptfs config when missing.
Declarative Encrypted Mounts
modules/tags/acme.nix
Adds systemd.mounts and systemd.automounts to mount ${dbCryptPath}${dbPath} using fuse.gocryptfs with extpass=${dbExtpass},nosyslog and automount idle timeout.
ACME Service Mount Binding
modules/tags/acme.nix
step-ca-acme now uses unitConfig.RequiresMountsFor = [dbPath] instead of requiring the old mount unit.
Socket-Activated Proxy and Socket
modules/tags/acme.nix
Adds step-ca-proxy (systemd-socket-proxyd) with ExecStartPre readiness wait and systemd.sockets.step-ca-proxy listening on ${acme.port} (Accept = false).
System Filesystem and Services Setup
modules/tags/acme.nix, modules/tags/all.nix
Adds gocryptfs to system.fsPackages; converts services config to an attribute set preserving dbus.x11Support = false and enabling openssh.startWhenNeeded = true.
Documentation Update
docs/pki.md
Updates ACME deployment instructions to document step-ca-proxy.socket and on-demand ACME services instead of starting step-ca-db-mount.service and step-ca-acme.service.

Sequence Diagram(s)

sequenceDiagram
  participant Client as ACME Client
  participant Socket as step-ca-proxy.socket
  participant Proxy as step-ca-proxy
  participant Backend as Step CA Backend (127.0.0.1:port+1)
  participant gocryptfs as gocryptfs mount (dbPath)
  participant Badger as Badger DB (dbPath)
  Client->>Socket: connect to external ACME port
  Socket->>Proxy: socket-activates proxy
  Proxy->>Backend: forward requests (systemd-socket-proxyd)
  Proxy->>Backend: ExecStartPre polls backend port (nc)
  Backend->>gocryptfs: access decrypted dbPath
  gocryptfs->>Badger: read/write Badger DB
  Proxy->>Client: proxied responses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • attilaolah/homelab#687: Introduces initial ACME Badger DB encryption with gocryptfs and TPM-sealed key; this PR refactors mount/unit/socket orchestration from that foundation.

Poem

🐰 A proxy stands guard at the gate so fair,
With gocryptfs keys floating through the air,
TPM seals the secrets, systemd unlocks,
Socket activation wakes services on knocks,
The backend rests safe on localhost's shore.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main change: enabling socket-based on-demand activation for low-traffic services (ACME and OpenSSH), which is the core objective of all three files modified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch socket-activate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@attilaolah

Copy link
Copy Markdown
Owner Author

@coderabbitai regarding the auto-generated description:

OpenSSH can be configured for on-demand startup

Actually, sshd is socket-activated, via sshd.socket, that's what the setting is actually doing. So ACME and SSHD are socket-activated, and the underlying mount for the Badger-DB is not just Systemd-integrated, but also auto-mounted and has an idle timeout. When no ACME requests are coming, even the FUSE mount is turned off.

This makes sense because:

  • ACME certs are refreshed only once every two days
  • SSH is used on-demand, usually also once every few days to perform updates or ad-hoc tasks

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

@attilaolah, thanks for the clarification! You're right — the auto-generated summary was inaccurate. Here's what it should reflect:

  • ACME and OpenSSH (sshd) are both socket-activated via systemd socket units (sshd.socket), meaning they start on-demand only when a connection arrives — not merely "can be configured" for it.
  • The BadgerDB FUSE mount has an idle timeout: it is auto-mounted by systemd and automatically unmounts when there are no active ACME requests, so the FUSE mount is also off during idle periods.

I'll regenerate the summary with this context.

The PR summary needs correction: (1) sshd IS socket-activated via sshd.socket (not merely "can be configured" for on-demand startup). (2) Both ACME and SSHD are socket-activated. (3) The BadgerDB FUSE mount is not just systemd-integrated — it is auto-mounted with an idle timeout, so the FUSE mount shuts down entirely when there are no ACME requests. ACME certs refresh ~every 2 days; SSH is used on-demand every few days.


✏️ Learnings added
Learnt from: attilaolah
URL: https://github.com/attilaolah/homelab/pull/688

Timestamp: 2026-05-10T19:21:41.149Z
Learning: In the `attilaolah/homelab` repository, PR `#688` introduces on-demand (socket) activation for low-traffic services. Both the ACME service and OpenSSH (`sshd`) are socket-activated via systemd socket units (e.g., `sshd.socket`) — they start only when a connection arrives. The BadgerDB FUSE mount is auto-mounted by systemd and has an idle timeout: it automatically unmounts when there are no active ACME requests. ACME certs are refreshed roughly every two days; SSH is used on-demand every few days.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Summary regeneration triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
modules/tags/acme.nix (1)

17-37: ⚡ Quick win

Avoid materializing the unsealed DB key on disk.

The current dbExtpass writes the unsealed key into a mktemp file under /run and then cats it. Even on tmpfs this is unnecessary disk exposure (and a SIGKILL leaves the file behind until reboot). tpm_unsealdata writes to stdout by default, so the helper can be a single exec that streams the key directly without touching the filesystem.

♻️ Proposed refactor
   dbExtpass = pkgs.writeShellApplication {
     name = "step-ca-db-extpass";
     runtimeInputs = with pkgs; [coreutils tpm-tools];
     text = ''
       set -euo pipefail

       if [[ ! -s ${dbKeySealed} ]]; then
         echo "sealed DB key is missing (${dbKeySealed})" >&2
         exit 1
       fi

-      key="$(mktemp /run/step-ca-db-key.XXXXXX)"
-      cleanup() {
-        rm -f "$key"
-      }
-      trap cleanup EXIT
-
-      tpm_unsealdata -z -i ${dbKeySealed} -o "$key"
-      cat "$key"
+      exec tpm_unsealdata -z -i ${dbKeySealed}
     '';
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/tags/acme.nix` around lines 17 - 37, The helper currently
materializes the unsealed DB key to a temporary file; update the dbExtpass
script to avoid creating a temp file by removing mktemp, cleanup(), trap, and
cat, and instead directly exec tpm_unsealdata -z -i ${dbKeySealed} so the
command streams the key to stdout (retain the initial existence check and set
-euo pipefail); reference dbExtpass, dbKeySealed and tpm_unsealdata when making
this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@modules/tags/acme.nix`:
- Around line 17-37: The helper currently materializes the unsealed DB key to a
temporary file; update the dbExtpass script to avoid creating a temp file by
removing mktemp, cleanup(), trap, and cat, and instead directly exec
tpm_unsealdata -z -i ${dbKeySealed} so the command streams the key to stdout
(retain the initial existence check and set -euo pipefail); reference dbExtpass,
dbKeySealed and tpm_unsealdata when making this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54a90818-1a97-4e3c-a5a2-dca50b3e5e68

📥 Commits

Reviewing files that changed from the base of the PR and between e304d77 and 3780b1b.

📒 Files selected for processing (3)
  • docs/pki.md
  • modules/tags/acme.nix
  • modules/tags/all.nix

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
modules/tags/acme.nix (1)

96-96: 💤 Low value

path on these services is unused.

For step-ca-db-prepare, the script invokes gocryptfs via lib.getExe' pkgs.gocryptfs "gocryptfs" (absolute), so path = [gocryptfs] doesn't contribute. For step-ca-proxy, ExecStartPre is a self-contained writeShellApplication with its own runtimeInputs, and ExecStart uses the absolute ${pkgs.systemd}/lib/systemd/systemd-socket-proxyd path — nothing consults the unit's $PATH. These can be dropped to reduce noise, or kept as a deliberate safety net if you prefer.

♻️ Optional cleanup
-        path = with pkgs; [gocryptfs];
         unitConfig.StopWhenUnneeded = true;
-        path = with pkgs; [coreutils netcat-openbsd];
-
         serviceConfig = {

Also applies to: 159-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/tags/acme.nix` at line 96, The `path = with pkgs; [gocryptfs]`
entries are dead/unused for the services `step-ca-db-prepare` and
`step-ca-proxy` because `step-ca-db-prepare` calls gocryptfs via an absolute
lib.getExe pkgs.gocryptfs invocation and `step-ca-proxy` uses a
writeShellApplication with its own runtimeInputs and ExecStart pointing to
${pkgs.systemd}/lib/systemd/systemd-socket-proxyd, so remove the `path =
[gocryptfs]` lines (or explicitly document why you keep them) from the service
definitions to reduce noise; look for the `path` attribute in the
`step-ca-db-prepare` and `step-ca-proxy` service blocks and delete it (ensure
there are no other references to `path` in those units such as relying on unit
$PATH or runtimeInputs before removing).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/tags/acme.nix`:
- Around line 86-96: The hardcoded before = ["var-lib-step\\x2dca-db.mount"] in
the step-ca-db-prepare block is brittle and redundant because the mount unit
name is derived from dbPath and the mount unit already Requires/After
step-ca-db-prepare.service; remove the entire before attribute from the
step-ca-db-prepare declaration to avoid breakage when dbPath changes.
Alternatively, if you really want explicit bidirectional ordering, add utils to
the module args and change the before value to ["${utils.escapeSystemdPath
dbPath}.mount"] so the unit name is generated from dbPath (but prefer removing
the redundant before).

---

Nitpick comments:
In `@modules/tags/acme.nix`:
- Line 96: The `path = with pkgs; [gocryptfs]` entries are dead/unused for the
services `step-ca-db-prepare` and `step-ca-proxy` because `step-ca-db-prepare`
calls gocryptfs via an absolute lib.getExe pkgs.gocryptfs invocation and
`step-ca-proxy` uses a writeShellApplication with its own runtimeInputs and
ExecStart pointing to ${pkgs.systemd}/lib/systemd/systemd-socket-proxyd, so
remove the `path = [gocryptfs]` lines (or explicitly document why you keep them)
from the service definitions to reduce noise; look for the `path` attribute in
the `step-ca-db-prepare` and `step-ca-proxy` service blocks and delete it
(ensure there are no other references to `path` in those units such as relying
on unit $PATH or runtimeInputs before removing).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba874a32-a54b-41c8-afef-9cac4dd01d08

📥 Commits

Reviewing files that changed from the base of the PR and between 3780b1b and 4a0b291.

📒 Files selected for processing (1)
  • modules/tags/acme.nix

Comment thread modules/tags/acme.nix Outdated
@sonarqubecloud

Copy link
Copy Markdown

@attilaolah
attilaolah merged commit 27dce35 into main May 11, 2026
4 checks passed
@attilaolah
attilaolah deleted the socket-activate branch May 11, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant