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};