BIT-29 Provision Bitcoin Pulse PostgreSQL (#5)
## Summary - Provision a digest-pinned PostgreSQL 18 container through rootless Podman with health-gated systemd readiness, loopback-only publishing, graceful shutdown, and the persistent `bitcoin-pulse-db` volume. - Declare separate SOPS credential paths for the administrator, migration, and application roles, stage them privately for first-start initialization, and reuse the role bootstrap SQL from the pinned BitcoinPulseAPI input. - Document human-managed secret provisioning, least-privilege verification, container recreation, graceful shutdown, and coordinated credential rotation. ## Validation - `tobserver-agent check` passed: - `nixpkgs-fmt --check` - `git diff --check` - `nix flake check --no-build` - Tobserver system derivation path evaluation - Gitleaks patch scan - Evaluated the OCI container, systemd service, rootless user, and NixOS assertions; all assertions pass and the generated configuration contains the expected loopback port, health readiness, persistent volume, and shutdown settings. ## Risks and limitations - Runtime startup, shutdown, persistence, and role behavior require Linux deployment verification and were not executed on macOS. - The encrypted SOPS keys are intentionally absent from this agent-authored change; the service must not be deployed before a human provisions them. - Static uid/gid 400 is reserved for the dedicated rootless container account and should be checked for conflicts during review. ## Manual follow-up - Add `bitcoin-pulse/postgres/admin-password`, `bitcoin-pulse/postgres/migration-password`, and `bitcoin-pulse/postgres/app-password` to `secrets/tobserver.yaml` using `sops`; commit only encrypted values. - Review and merge this pull request. - Deploy manually on Tobserver after merge and encrypted secret provisioning. - Run the startup, loopback binding, role privilege, restart/persistence, and graceful-shutdown checks in `docs/bitcoin-pulse-postgresql.md`. Reviewed-on: https://codeberg.org/oibot/Tobserver/pulls/5
This commit is contained in:
parent
2711f66654
commit
55fb51c760
2 changed files with 302 additions and 3 deletions
|
|
@ -1,15 +1,165 @@
|
|||
{ bitcoin-pulse, ... }:
|
||||
{ bitcoin-pulse, config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
containerName = "bitcoin-pulse-postgres";
|
||||
serviceName = "podman-${containerName}";
|
||||
runtimeDirectory = "/run/${containerName}";
|
||||
|
||||
secretFiles = {
|
||||
admin = config.sops.secrets."bitcoin-pulse/postgres/admin-password".path;
|
||||
app = config.sops.secrets."bitcoin-pulse/postgres/app-password".path;
|
||||
migration = config.sops.secrets."bitcoin-pulse/postgres/migration-password".path;
|
||||
};
|
||||
|
||||
prepareSecrets = pkgs.writeShellApplication {
|
||||
name = "bitcoin-pulse-postgresql-prepare-secrets";
|
||||
runtimeInputs = [
|
||||
config.virtualisation.podman.package
|
||||
pkgs.coreutils
|
||||
];
|
||||
text = ''
|
||||
copy_secret() {
|
||||
source="$1"
|
||||
destination="$2"
|
||||
|
||||
podman unshare rm -f "$destination"
|
||||
# PostgreSQL runs as uid 999 in the container. Store each runtime copy
|
||||
# with the corresponding rootless subordinate uid and a private mode.
|
||||
podman unshare install -m 0400 -o 999 -g 999 "$source" "$destination"
|
||||
}
|
||||
|
||||
copy_secret ${lib.escapeShellArg secretFiles.admin} \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/admin-password"}
|
||||
copy_secret ${lib.escapeShellArg secretFiles.app} \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/app-password"}
|
||||
copy_secret ${lib.escapeShellArg secretFiles.migration} \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/migration-password"}
|
||||
'';
|
||||
};
|
||||
|
||||
cleanupSecrets = pkgs.writeShellApplication {
|
||||
name = "bitcoin-pulse-postgresql-cleanup-secrets";
|
||||
runtimeInputs = [
|
||||
config.virtualisation.podman.package
|
||||
pkgs.coreutils
|
||||
];
|
||||
text = ''
|
||||
podman unshare rm -f \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/admin-password"} \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/app-password"} \
|
||||
${lib.escapeShellArg "${runtimeDirectory}/migration-password"}
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ bitcoin-pulse.nixosModules.default ];
|
||||
|
||||
services.bitcoin-pulse = {
|
||||
# BIT-28 and BIT-29 will enable the production instance after PostgreSQL,
|
||||
# migrations, and credentials have been configured.
|
||||
# BIT-28 will enable the production instance after PostgreSQL readiness
|
||||
# and migrations have been integrated with these credentials.
|
||||
enable = false;
|
||||
|
||||
bindAddress = "127.0.0.1";
|
||||
port = 3000;
|
||||
mempool.baseUrl = "http://127.0.0.1:8999";
|
||||
};
|
||||
|
||||
users.groups = {
|
||||
bitcoin-pulse = { };
|
||||
bitcoin-pulse-migration = { };
|
||||
bitcoin-pulse-postgres.gid = 400;
|
||||
};
|
||||
|
||||
users.users.bitcoin-pulse-postgres = {
|
||||
description = "Rootless PostgreSQL container account for Bitcoin Pulse";
|
||||
isSystemUser = true;
|
||||
uid = 400;
|
||||
group = "bitcoin-pulse-postgres";
|
||||
extraGroups = [
|
||||
"bitcoin-pulse"
|
||||
"bitcoin-pulse-migration"
|
||||
];
|
||||
home = "/var/lib/bitcoin-pulse-postgres";
|
||||
homeMode = "0700";
|
||||
createHome = true;
|
||||
linger = true;
|
||||
autoSubUidGidRange = true;
|
||||
};
|
||||
|
||||
sops.secrets = {
|
||||
"bitcoin-pulse/postgres/admin-password" = {
|
||||
group = "bitcoin-pulse-postgres";
|
||||
mode = "0440";
|
||||
};
|
||||
|
||||
"bitcoin-pulse/postgres/app-password" = {
|
||||
group = "bitcoin-pulse";
|
||||
mode = "0440";
|
||||
};
|
||||
|
||||
"bitcoin-pulse/postgres/migration-password" = {
|
||||
group = "bitcoin-pulse-migration";
|
||||
mode = "0440";
|
||||
};
|
||||
};
|
||||
|
||||
virtualisation.oci-containers = {
|
||||
backend = "podman";
|
||||
|
||||
containers.${containerName} = {
|
||||
# Official PostgreSQL 18.4 Bookworm multi-architecture image, pinned to
|
||||
# the immutable digest tested by BitcoinPulseAPI on 2026-07-16.
|
||||
image = "docker.io/library/postgres@sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296";
|
||||
pull = "missing";
|
||||
autoStart = true;
|
||||
autoRemoveOnStop = true;
|
||||
|
||||
podman = {
|
||||
user = "bitcoin-pulse-postgres";
|
||||
sdnotify = "healthy";
|
||||
};
|
||||
|
||||
environment = {
|
||||
POSTGRES_DB = "bitcoin_pulse";
|
||||
POSTGRES_USER = "bitcoin_pulse_admin";
|
||||
POSTGRES_PASSWORD_FILE = "/run/secrets/admin-password";
|
||||
POSTGRES_HOST_AUTH_METHOD = "scram-sha-256";
|
||||
POSTGRES_INITDB_ARGS = "--auth-host=scram-sha-256 --data-checksums";
|
||||
};
|
||||
|
||||
ports = [ "127.0.0.1:5432:5432" ];
|
||||
|
||||
volumes = [
|
||||
"bitcoin-pulse-db:/var/lib/postgresql"
|
||||
"${runtimeDirectory}/admin-password:/run/secrets/admin-password:ro"
|
||||
"${runtimeDirectory}/app-password:/run/secrets/app-password:ro"
|
||||
"${runtimeDirectory}/migration-password:/run/secrets/migration-password:ro"
|
||||
"${bitcoin-pulse}/resources/postgres/initdb/010-roles.sql:/docker-entrypoint-initdb.d/010-roles.sql:ro"
|
||||
];
|
||||
|
||||
extraOptions = [
|
||||
"--health-cmd=pg_isready --host=127.0.0.1 --port=5432 --username=bitcoin_pulse_admin --dbname=bitcoin_pulse"
|
||||
"--health-interval=10s"
|
||||
"--health-timeout=5s"
|
||||
"--health-retries=6"
|
||||
"--health-start-period=60s"
|
||||
"--stop-signal=SIGINT"
|
||||
"--stop-timeout=60"
|
||||
"--pids-limit=256"
|
||||
"--security-opt=no-new-privileges"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.${serviceName}.serviceConfig = {
|
||||
ExecStartPre = lib.mkAfter [ "${prepareSecrets}/bin/bitcoin-pulse-postgresql-prepare-secrets" ];
|
||||
ExecStartPost = [ "${cleanupSecrets}/bin/bitcoin-pulse-postgresql-cleanup-secrets" ];
|
||||
ExecStopPost = lib.mkAfter [ "${cleanupSecrets}/bin/bitcoin-pulse-postgresql-cleanup-secrets" ];
|
||||
RuntimeDirectoryMode = "0700";
|
||||
RestartSec = "5s";
|
||||
TimeoutStopSec = lib.mkForce "90s";
|
||||
UMask = "0077";
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.postgresql_18 ];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue