BareGit

Add Arch package and configurable unix-socket permission

Ship a -git PKGBUILD with sysusers, tmpfiles, and a systemd unit. The
unit binds the daemon to /run/overseer/overseer.sock (managed by
RuntimeDirectory) so any reverse proxy can be plugged in without IP/port
config. To let the proxy connect regardless of its user/group, add a new
`socket_permission` config key — parsed as octal, plumbed into
mw::SocketFileInfo::permission — defaulted to 0666 in the packaged yaml.
Author: MetroWind <chris.corsair@gmail.com>
Date: Sun May 17 11:15:01 2026 -0700
Commit: abbc757cf3903140b0d29eb851df3098c5dee4ca

Changes

diff --git a/packages/arch/PKGBUILD b/packages/arch/PKGBUILD
new file mode 100644
index 0000000..3afd515
--- /dev/null
+++ b/packages/arch/PKGBUILD
@@ -0,0 +1,57 @@
+pkgname=overseer-git
+pkgver=r1.1234567
+pkgrel=1
+pkgdesc="Self-hosted family information system (inventory, OIDC, htmx UI)."
+arch=('x86_64')
+url="https://github.com/MetroWind/home-info"
+license=('unknown')
+depends=('gcc-libs' 'glibc' 'openssl' 'sqlite' 'curl' 'zlib'
+         'imagemagick' 'systemd-libs')
+makedepends=('cmake' 'git' 'pkgconf')
+backup=('etc/overseer.yaml')
+source=("git+$url.git"
+        "overseer.service"
+        "overseer.sysusers"
+        "overseer.tmpfiles"
+        "overseer.yaml")
+sha256sums=('SKIP'
+            'SKIP'
+            'SKIP'
+            'SKIP'
+            'SKIP')
+
+pkgver() {
+  cd "home-info"
+  printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
+}
+
+build() {
+  cmake -B build -S "home-info" \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DOVERSEER_BUILD_TEST=OFF
+  cmake --build build -j "$(nproc)"
+}
+
+package() {
+  install -Dm755 build/overseer "$pkgdir/usr/bin/overseer"
+
+  install -Dm644 overseer.service  "$pkgdir/usr/lib/systemd/system/overseer.service"
+  install -Dm644 overseer.sysusers "$pkgdir/usr/lib/sysusers.d/overseer.conf"
+  install -Dm644 overseer.tmpfiles "$pkgdir/usr/lib/tmpfiles.d/overseer.conf"
+
+  install -Dm640 overseer.yaml "$pkgdir/etc/overseer.yaml"
+
+  cd "home-info"
+  # Templates: <template-dir>/inventory/templates/*.inja — the service
+  # passes --template-dir=/usr/share/overseer/modules so the layout
+  # mirrors the source tree.
+  install -d "$pkgdir/usr/share/overseer"
+  cp -r src/overseer/modules "$pkgdir/usr/share/overseer/modules"
+  cp -r src/static           "$pkgdir/usr/share/overseer/static"
+
+  # Strip non-template sources that landed under modules/ (we only need
+  # the .inja files at runtime).
+  find "$pkgdir/usr/share/overseer/modules" \
+    -type f ! -name '*.inja' -delete
+  find "$pkgdir/usr/share/overseer/modules" -type d -empty -delete
+}
diff --git a/packages/arch/overseer.service b/packages/arch/overseer.service
new file mode 100644
index 0000000..437a6e9
--- /dev/null
+++ b/packages/arch/overseer.service
@@ -0,0 +1,34 @@
+[Unit]
+Description=Overseer family information system
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=overseer
+Group=overseer
+ExecStart=/usr/bin/overseer \
+    --config /etc/overseer.yaml \
+    --template-dir /usr/share/overseer/modules \
+    --static-dir /usr/share/overseer/static
+WorkingDirectory=/var/lib/overseer
+RuntimeDirectory=overseer
+RuntimeDirectoryMode=0755
+Restart=on-failure
+RestartSec=5
+
+# Hardening
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/var/lib/overseer
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectControlGroups=true
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+LockPersonality=true
+RestrictRealtime=true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/packages/arch/overseer.sysusers b/packages/arch/overseer.sysusers
new file mode 100644
index 0000000..bea6b96
--- /dev/null
+++ b/packages/arch/overseer.sysusers
@@ -0,0 +1 @@
+u overseer - "Overseer family info system" /var/lib/overseer
diff --git a/packages/arch/overseer.tmpfiles b/packages/arch/overseer.tmpfiles
new file mode 100644
index 0000000..8e3ff12
--- /dev/null
+++ b/packages/arch/overseer.tmpfiles
@@ -0,0 +1,2 @@
+d /var/lib/overseer 0750 overseer overseer -
+z /etc/overseer.yaml 0640 root overseer -
diff --git a/packages/arch/overseer.yaml b/packages/arch/overseer.yaml
new file mode 100644
index 0000000..6ab346f
--- /dev/null
+++ b/packages/arch/overseer.yaml
@@ -0,0 +1,19 @@
+# /etc/overseer.yaml — installed by the overseer package.
+# Mode SHOULD be 0640 root:overseer (the tmpfiles rule enforces this);
+# the OIDC client_secret is sensitive.
+
+bind_address: "unix:/run/overseer/overseer.sock"
+port: 8080                          # ignored when bind_address starts with "unix:"
+socket_permission: "0666"           # world rw — so any reverse proxy can connect
+log_level: "info"                   # trace|debug|info|warn|error|critical
+db_path: "/var/lib/overseer/overseer.db"
+
+oidc:
+    issuer_url:    "https://keycloak.example.com/realms/family"
+    client_id:     "overseer"
+    client_secret: "REPLACE_ME"
+    redirect_uri:  "https://overseer.example.com/oidc/callback"
+
+session:
+    cookie_name: "overseer_sid"
+    max_age_minutes: 480
diff --git a/src/overseer/config.cpp b/src/overseer/config.cpp
index 6694e5c..1b4b94a 100644
--- a/src/overseer/config.cpp
+++ b/src/overseer/config.cpp
@@ -76,6 +76,26 @@ mw::E<Config> loadConfig(const std::filesystem::path& path)
     {
         c.port = root["port"].as<int>();
     }
+    if(root["socket_permission"])
+    {
+        // Accept either a YAML integer (e.g. 438) or a string of octal
+        // digits (e.g. "0666"). Strings are always parsed as octal so
+        // the natural "0666" form does the obvious thing despite YAML
+        // 1.2 treating a bare 0666 as decimal.
+        auto node = root["socket_permission"];
+        try
+        {
+            std::string s = node.as<std::string>();
+            c.socket_permission = static_cast<unsigned int>(
+                std::stoul(s, nullptr, 8));
+        }
+        catch(const std::exception& e)
+        {
+            return std::unexpected(mw::runtimeError(std::format(
+                "Invalid socket_permission '{}': {}",
+                node.as<std::string>(), e.what())));
+        }
+    }
     if(root["log_level"])
     {
         c.log_level = root["log_level"].as<std::string>();
diff --git a/src/overseer/config.hpp b/src/overseer/config.hpp
index 9a339a9..5cc4509 100644
--- a/src/overseer/config.hpp
+++ b/src/overseer/config.hpp
@@ -2,6 +2,7 @@
 
 #include <chrono>
 #include <filesystem>
+#include <optional>
 #include <string>
 
 #include <mw/error.hpp>
@@ -27,6 +28,9 @@ struct Config
 {
     std::string bind_address = "127.0.0.1";
     int port = 8080;
+    // Permission bits applied to the unix socket file when
+    // bind_address starts with "unix:". Unused otherwise.
+    std::optional<unsigned int> socket_permission;
     std::string log_level = "info";
     std::string db_path = "/var/lib/overseer/overseer.db";
     OIDCConfig oidc;
diff --git a/src/overseer/server.cpp b/src/overseer/server.cpp
index 1120c90..41b706b 100644
--- a/src/overseer/server.cpp
+++ b/src/overseer/server.cpp
@@ -27,6 +27,7 @@ buildListenAddress(const Config& config)
     {
         mw::SocketFileInfo info(
             std::string_view(config.bind_address).substr(5));
+        info.permission = config.socket_permission;
         return info;
     }
     return mw::IPSocketInfo{config.bind_address, config.port};