diff --git a/.gitignore b/.gitignore
index 2f09591c7766..819e5555787d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,6 +120,16 @@ deploy/overlays/provisiond/etc/imports/*
deploy/overlays/pollerd/etc/poller-services.yaml
deploy/overlays/perspectivepollerd/etc/poller-services.yaml
+# SNMP peer configuration — the single source of truth is committed at
+# overlays/shared/etc/snmp-config.xml; build.sh stages a copy into each
+# SNMP-consuming daemon's overlay at image-build time. Those copies are
+# generated, never committed (see tools/build.sh stage_shared_snmp_config).
+deploy/overlays/collectd/etc/snmp-config.xml
+deploy/overlays/pollerd/etc/snmp-config.xml
+deploy/overlays/provisiond/etc/snmp-config.xml
+deploy/overlays/enlinkd/etc/snmp-config.xml
+deploy/overlays/perspectivepollerd/etc/snmp-config.xml
+
# C4 diagram export tooling (npm deps)
docs/architecture/c4/scripts/node_modules/
# Structurizr Lite regenerable artifacts (layout cache + compiled workspace)
diff --git a/deploy/overlays/collectd/etc/snmp-config.xml b/deploy/overlays/collectd/etc/snmp-config.xml
deleted file mode 100644
index 75392e8ba433..000000000000
--- a/deploy/overlays/collectd/etc/snmp-config.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/deploy/overlays/enlinkd/etc/snmp-config.xml b/deploy/overlays/enlinkd/etc/snmp-config.xml
deleted file mode 100644
index 75392e8ba433..000000000000
--- a/deploy/overlays/enlinkd/etc/snmp-config.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/deploy/overlays/pollerd/etc/snmp-config.xml b/deploy/overlays/pollerd/etc/snmp-config.xml
deleted file mode 100644
index 3215705e2e40..000000000000
--- a/deploy/overlays/pollerd/etc/snmp-config.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/deploy/overlays/provisiond/etc/snmp-config.xml b/deploy/overlays/provisiond/etc/snmp-config.xml
deleted file mode 100644
index 3215705e2e40..000000000000
--- a/deploy/overlays/provisiond/etc/snmp-config.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/deploy/overlays/perspectivepollerd/etc/snmp-config.xml b/deploy/overlays/shared/etc/snmp-config.xml
similarity index 100%
rename from deploy/overlays/perspectivepollerd/etc/snmp-config.xml
rename to deploy/overlays/shared/etc/snmp-config.xml
diff --git a/tools/build.sh b/tools/build.sh
index 36dc479a85fe..c555055d7436 100755
--- a/tools/build.sh
+++ b/tools/build.sh
@@ -45,10 +45,19 @@ DAEMON_NAMES="alarmd bsmd collectd discovery enlinkd eventtranslator perspective
# consumer's overlay at image-build time (per-daemon copies are git-ignored).
CATALOG_CONSUMERS="pollerd perspectivepollerd"
-# shellcheck disable=SC1091
-. "$SCRIPT_DIR/version.sh"
-deltav_resolve_version
-deltav_sync_env_version
+# Daemons that consume the SNMP peer configuration. The single source of truth
+# is committed at overlays/shared/etc/snmp-config.xml and staged into each
+# consumer's overlay at image-build time (per-daemon copies are git-ignored).
+SNMP_CONSUMERS="collectd pollerd provisiond enlinkd perspectivepollerd"
+
+# Resolve the build version only when executed directly (not when sourced by a
+# test). Sourcing build.sh then exposes its functions without side effects.
+if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
+ # shellcheck disable=SC1091
+ . "$SCRIPT_DIR/version.sh"
+ deltav_resolve_version
+ deltav_sync_env_version
+fi
log() { echo "==> $*"; }
err() { echo "ERROR: $*" >&2; exit 1; }
@@ -69,6 +78,22 @@ stage_shared_poller_catalog() {
done
}
+# Stage the single shared SNMP peer configuration into each SNMP-consuming
+# daemon's overlay (overlays//etc/snmp-config.xml) so the per-daemon
+# Docker COPY and the Dockerfile lint stage pick it up. The destination files
+# are git-ignored generated artifacts; overlays/shared/etc/snmp-config.xml is
+# the committed source of truth. Idempotent — safe to run before every build.
+stage_shared_snmp_config() {
+ local shared="$DEPLOY_DIR/overlays/shared/etc/snmp-config.xml"
+ [ -f "$shared" ] || err "shared snmp config not found: $shared"
+ local name
+ for name in $SNMP_CONSUMERS; do
+ mkdir -p "$DEPLOY_DIR/overlays/$name/etc"
+ cp "$shared" "$DEPLOY_DIR/overlays/$name/etc/snmp-config.xml"
+ log " staged shared snmp config -> overlays/$name/etc/snmp-config.xml"
+ done
+}
+
# build_image SHORT_NAME -f DOCKERFILE [docker-build-args...] CONTEXT
# Tags ${IMAGE_PREFIX}/SHORT_NAME at :$VERSION and :latest. Uses buildx;
# --load for local single-arch, --platform/--push when PUSH=true.
@@ -380,6 +405,10 @@ do_deltav_images() {
# before the per-daemon COPY/lint runs.
stage_shared_poller_catalog
+ # Stage the shared SNMP peer config into every SNMP-consuming daemon overlay
+ # before the per-daemon COPY/lint runs.
+ stage_shared_snmp_config
+
# Phase 2: Build daemon-base image
log "Building ${IMAGE_PREFIX}/daemon-base:$VERSION..."
build_image daemon-base --no-cache \
@@ -556,6 +585,10 @@ do_single_daemon_image() {
# perspectivepollerd bakes the current catalog (and lints it).
stage_shared_poller_catalog
+ # Stage the shared SNMP peer config so a single-daemon rebuild of an
+ # SNMP-consuming daemon bakes the current config (and lints it).
+ stage_shared_snmp_config
+
# compute-shared-libs.sh re-derives the shared/unique library split — it
# needs every daemon's fat JAR present — and repopulates staging/. This is
# unzip/copy work, not a compile.
@@ -664,4 +697,8 @@ main() {
esac
}
-main "$@"
+# Run the CLI dispatcher only when executed directly; sourcing (e.g. from a
+# test) loads the functions above without invoking a build.
+if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
+ main "$@"
+fi
diff --git a/tools/stage-shared-snmp-config.test.sh b/tools/stage-shared-snmp-config.test.sh
new file mode 100644
index 000000000000..a1ffc1681e04
--- /dev/null
+++ b/tools/stage-shared-snmp-config.test.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# Copyright 2026 Ronny Trommer
+# SPDX-License-Identifier: Apache-2.0
+#
+# Standalone test for tools/build.sh stage_shared_snmp_config().
+# Drift guard: the committed shared snmp-config.xml must fan out byte-identically
+# into every SNMP-consuming daemon overlay. Run: bash tools/stage-shared-snmp-config.test.sh
+set -u
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PASS=0; FAIL=0
+check() { # desc expected actual
+ if [ "$2" = "$3" ]; then echo " PASS: $1"; PASS=$((PASS+1));
+ else echo " FAIL: $1 (expected='$2' actual='$3')"; FAIL=$((FAIL+1)); fi
+}
+
+# Source build.sh for its functions only (the BASH_SOURCE guard skips version
+# resolution and main, so sourcing has no side effects).
+# shellcheck disable=SC1091
+. "$HERE/build.sh"
+# build.sh enables `set -euo pipefail`; this test handles non-zero exits itself
+# (it deliberately triggers a failure path), so relax -e/pipefail but keep -u.
+set +e +o pipefail
+
+check "SNMP_CONSUMERS list" "collectd pollerd provisiond enlinkd perspectivepollerd" "$SNMP_CONSUMERS"
+
+# Stage the real committed shared source into an isolated temp overlay tree.
+SRC="$HERE/../deploy/overlays/shared/etc/snmp-config.xml"
+check "shared source exists" "yes" "$([ -f "$SRC" ] && echo yes || echo no)"
+
+TMP="$(mktemp -d)"
+mkdir -p "$TMP/overlays/shared/etc"
+cp "$SRC" "$TMP/overlays/shared/etc/snmp-config.xml"
+DEPLOY_DIR="$TMP" stage_shared_snmp_config >/dev/null
+DEPLOY_DIR="$TMP" stage_shared_snmp_config >/dev/null # idempotent: run twice
+
+for name in $SNMP_CONSUMERS; do
+ if cmp -s "$SRC" "$TMP/overlays/$name/etc/snmp-config.xml"; then
+ check "staged $name byte-identical" "ok" "ok"
+ else
+ check "staged $name byte-identical" "ok" "differs-or-missing"
+ fi
+done
+
+# Fail-fast when the shared source is absent. Run in a subshell because err()
+# calls exit, which would otherwise terminate this test process.
+EMPTY="$(mktemp -d)"
+( DEPLOY_DIR="$EMPTY" stage_shared_snmp_config ) >/dev/null 2>&1
+check "missing source fails fast" "fail" "$([ $? -ne 0 ] && echo fail || echo passed)"
+
+rm -rf "$TMP" "$EMPTY"
+
+echo "── $PASS passed, $FAIL failed ──"
+[ "$FAIL" -eq 0 ]