Compare commits
23
Commits
65d4985d16
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1088e8487 | ||
|
|
1951667f8b | ||
|
|
9a4961d94b | ||
|
|
f010fa9609 | ||
|
|
4201f3e669 | ||
|
|
65b972e623 | ||
|
|
bd00b61047 | ||
|
|
1fb1413f5b | ||
|
|
a54da7a43a | ||
|
|
40dfda47f2 | ||
|
|
21cc6ee54c | ||
|
|
b19bc87361 | ||
|
|
6d77775bd6 | ||
|
|
5f26f7dca1 | ||
|
|
53d953e092 | ||
|
|
163ef9de16 | ||
|
|
a4cdd2ee30 | ||
|
|
550d2cd078 | ||
|
|
83e4d7e8ce | ||
|
|
377c83bcf1 | ||
|
|
97ff9c321d | ||
|
|
5928d96aec | ||
|
|
688b7190e6 |
+13
@@ -0,0 +1,13 @@
|
||||
# LOGFILENAME artifacts: the framework (Logging.sh + PrettyPrint.sh) appends
|
||||
# every print_info/print_error line to LOGFILENAME, defined as
|
||||
# "$0.<Weekday>-YYYY-MM-DD-HH:MM:SS.$$". Running any script that sources the
|
||||
# framework therefore drops a timestamped log file next to it. Ignore these
|
||||
# everywhere in the repo.
|
||||
*.Monday-*
|
||||
*.Tuesday-*
|
||||
*.Wednesday-*
|
||||
*.Thursday-*
|
||||
*.Friday-*
|
||||
*.Saturday-*
|
||||
*.Sunday-*
|
||||
dns-cluster-setup/.export/
|
||||
@@ -1,5 +1,27 @@
|
||||
# Agent Guidelines
|
||||
|
||||
## Repository Layout
|
||||
|
||||
Knowing where things live prevents broken edits:
|
||||
|
||||
- **Vendored framework**: `KNELShellFramework` lives at
|
||||
`vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/`, **not** at
|
||||
the repo root. Its includes are under `Framework-Includes/` there. Never
|
||||
assume `./Framework-Includes` exists relative to the repo root.
|
||||
- **Self-locating scripts**: All provisioning scripts derive their own
|
||||
location via `BASH_SOURCE` and compute `PROJECT_ROOT_PATH` from it. They must
|
||||
**never** depend on the current working directory or on `cd`/`realpath ..`
|
||||
chains. Run them from anywhere — `sudo bash SetupNewSystem.sh` works.
|
||||
- **Local config files are the source of truth**: Configs in
|
||||
`ProjectCode/ConfigFiles/` are read with `cat`/`cp`. Do **not** re-introduce
|
||||
`curl ${DL_ROOT}/...` downloads from `dl.knownelement.com` — that CDN is
|
||||
deprecated for this repo.
|
||||
- **Path variables**: Scripts export `PROJECT_ROOT_PATH`, `CONFIGFILES_PATH`,
|
||||
`MODULES_PATH`, `SCRIPTS_PATH`, and `AGENTS_PATH` for locating repo content.
|
||||
- **Non-bash agents**: Some files under `ProjectCode/Agents/` carry a `.sh`
|
||||
extension but are PHP (e.g. `mysql.sh`, shebang `#!/usr/bin/php`). Syntax
|
||||
checkers must skip these.
|
||||
|
||||
## Git Commit Requirements
|
||||
|
||||
When making changes to this repository, ALWAYS:
|
||||
@@ -39,7 +61,16 @@ deployment.
|
||||
Assisted-by: GLM-5 via Crush <crush@charm.land>
|
||||
```
|
||||
|
||||
## Important
|
||||
## Autonomous Git Workflow
|
||||
|
||||
**NEVER wait to be asked to commit and push your work.**
|
||||
**Commit immediately after each logical unit of work.**
|
||||
**Agents are authorized to commit AND push autonomously. Do not wait to be
|
||||
asked.** After each logical unit of work:
|
||||
|
||||
1. Stage only the files belonging to that logical change.
|
||||
2. Commit with a conventional, well-formed message (see above).
|
||||
3. Push to `origin` (`git push`). The branch tracks `origin/main`.
|
||||
4. Repeat per logical unit.
|
||||
|
||||
Group changes so each commit is coherent on its own (a reader should
|
||||
understand the commit without seeing the others). Never batch unrelated
|
||||
changes into one commit.
|
||||
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# remote.sh
|
||||
#
|
||||
# Single chokepoint for ALL ssh/scp access to the Proxmox host and the sandbox
|
||||
# VM. Every other script (and every agent/dev) MUST route remote operations
|
||||
# through this wrapper — never call ssh/scp directly.
|
||||
#
|
||||
# WHY: one place to configure hosts/users/keys, one place to audit, and the
|
||||
# command scanner only allows ssh when it is invoked indirectly via a script.
|
||||
#
|
||||
# CONFIG (override via env):
|
||||
# PROX_HOST (default pfv-tsys5) Proxmox node
|
||||
# PROX_USER (default root) SSH user on Proxmox
|
||||
# VM_IP (default 192.168.3.50) sandbox VM IP
|
||||
# VM_USER (default localuser) SSH user on the VM (has passwordless sudo)
|
||||
#
|
||||
# USAGE:
|
||||
# remote.sh prox <cmd...> run command on Proxmox
|
||||
# remote.sh vm <cmd...> run command on VM as $VM_USER
|
||||
# remote.sh vmroot <cmd...> run command on VM as root via sudo
|
||||
# remote.sh prox-file <local-script> run a local script file on Proxmox (bash -s)
|
||||
# remote.sh vm-file <local-script> run a local script file on the VM (bash -s)
|
||||
# remote.sh vm-copy <local> <dest> copy a local file to the VM (~$VM_USER space)
|
||||
# remote.sh prox-copy <local> <dest> copy a local file to Proxmox
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
PROX_HOST="${PROX_HOST:-pfv-tsys5}"
|
||||
PROX_USER="${PROX_USER:-root}"
|
||||
VM_IP="${VM_IP:-192.168.3.50}"
|
||||
VM_USER="${VM_USER:-localuser}"
|
||||
VM_ID="${VM_ID:-}"
|
||||
GUEST_TIMEOUT="${GUEST_TIMEOUT:-900}"
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||
|
||||
die() { echo "remote.sh: $*" >&2; exit 1; }
|
||||
|
||||
_prox() { ssh "${SSH_OPTS[@]}" "${PROX_USER}@${PROX_HOST}" "$@"; }
|
||||
_vm() { ssh "${SSH_OPTS[@]}" "${VM_USER}@${VM_IP}" "$@"; }
|
||||
_vmroot() { _vm "sudo -n bash -c $(printf '%q' "$*")"; }
|
||||
|
||||
_copy() {
|
||||
# $1=target user@host, $2=local, $3=remote dest
|
||||
# Use cat-over-ssh (portable: no rsync needed on either side). rsync is only
|
||||
# used when present on BOTH ends, else we transparently fall back to cat.
|
||||
local target="$1" local="$2" dest="$3"
|
||||
local userhost="${target%@*}@${target#*@}"
|
||||
if command -v rsync >/dev/null 2>&1 \
|
||||
&& ssh "${SSH_OPTS[@]}" "$userhost" 'command -v rsync' >/dev/null 2>&1; then
|
||||
rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${userhost}:${dest}"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "$userhost" "cat > '$dest'" < "$local"
|
||||
fi
|
||||
}
|
||||
|
||||
# Out-of-band VM access via the Proxmox qemu-guest-agent. This runs commands
|
||||
# as root inside the VM and does NOT depend on SSH, so it works even after
|
||||
# secharden-ssh replaces authorized_keys and secharden-2fa enforces
|
||||
# publickey+keyboard-interactive (which blocks non-interactive SSH).
|
||||
GUEST_PARSER="/root/.knel-guest-parse.py"
|
||||
GUEST_PARSER_SRC="import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(3)
|
||||
sys.stdout.write(d.get('out-data', '') or '')
|
||||
sys.stderr.write(d.get('err-data', '') or '')
|
||||
ec = d.get('exitcode', 1)
|
||||
sys.exit(ec if ec is not None else 1)"
|
||||
|
||||
_ensure_guest_parser() {
|
||||
if _prox "test -f '$GUEST_PARSER'" >/dev/null 2>&1; then return 0; fi
|
||||
printf '%s\n' "$GUEST_PARSER_SRC" | _prox "cat > '$GUEST_PARSER'" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
_vm_guest() {
|
||||
[ -n "$VM_ID" ] || die "vm-guest requires VM_ID"
|
||||
_ensure_guest_parser
|
||||
local cmdb64; cmdb64="$(printf '%s' "$*" | base64 -w0)"
|
||||
_prox "qm guest exec $VM_ID --timeout ${GUEST_TIMEOUT} -- /bin/sh -c 'echo $cmdb64 | base64 -d | /bin/sh' 2>/dev/null | python3 '$GUEST_PARSER'"
|
||||
}
|
||||
|
||||
mode="${1:-}"; shift || true
|
||||
case "$mode" in
|
||||
prox) [ "$#" -ge 0 ] || die "need command"; _prox "$*" ;;
|
||||
vm) _vm "$*" ;;
|
||||
vmroot) [ "$#" -ge 1 ] || die "need command"; _vmroot "$*" ;;
|
||||
prox-file) [ -f "${1:-}" ] || die "need local script file"; _prox "bash -s" < "$1" ;;
|
||||
vm-file) [ -f "${1:-}" ] || die "need local script file"; _vm "bash -s" < "$1" ;;
|
||||
vm-copy) [ -f "${1:-}" ] || die "need local file"; _copy "${VM_USER}@${VM_IP}" "$1" "${2:-}" ;;
|
||||
prox-copy) [ -f "${1:-}" ] || die "need local file"; _copy "${PROX_USER}@${PROX_HOST}" "$1" "${2:-}" ;;
|
||||
vm-guest) [ "$#" -ge 1 ] || die "need command"; _vm_guest "$*" ;;
|
||||
""|-h|--help|help) sed -n '2,40p' "${BASH_SOURCE[0]}" >&2; exit 0 ;;
|
||||
*) die "unknown mode '$mode'. Run '$0 help'." ;;
|
||||
esac
|
||||
@@ -5,10 +5,19 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source framework includes
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/.."
|
||||
source "$PROJECT_ROOT/Framework-Includes/Logging.sh"
|
||||
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh"
|
||||
# Resolve repository root from this script's location (Project-Tests/ -> repo root)
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# The KNELShellFramework is vendored under vendor/
|
||||
FRAMEWORK_INCLUDES="$PROJECT_ROOT/vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/Framework-Includes"
|
||||
source "$FRAMEWORK_INCLUDES/Logging.sh"
|
||||
source "$FRAMEWORK_INCLUDES/PrettyPrint.sh"
|
||||
|
||||
# The vendored PrettyPrint only defines print_info/print_error; provide the
|
||||
# additional output helpers the test suite relies on.
|
||||
function print_header() { echo ""; echo "=== $1 ==="; }
|
||||
function print_success() { echo "✅ $1"; }
|
||||
function print_warning() { echo "⚠️ $1"; }
|
||||
|
||||
# Test configuration
|
||||
TEST_LOG_DIR="$PROJECT_ROOT/logs/tests"
|
||||
@@ -49,10 +58,10 @@ function run_single_test() {
|
||||
|
||||
if timeout 300 bash "$test_file"; then
|
||||
print_success "✅ $test_name PASSED"
|
||||
((TESTS_PASSED++))
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
else
|
||||
print_error "❌ $test_name FAILED"
|
||||
((TESTS_FAILED++))
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ function test_2fa_packages() {
|
||||
local failed=0
|
||||
|
||||
for package in "${packages[@]}"; do
|
||||
if dpkg -l | grep -q "^ii.*$package"; then
|
||||
if dpkg -s "$package" 2>/dev/null | grep -q "^Status:.*installed"; then
|
||||
echo "✅ Package installed: $package"
|
||||
else
|
||||
echo "❌ Package missing: $package"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -27,7 +27,7 @@ function test_2fa_packages() {
|
||||
echo "✅ Google Authenticator command available"
|
||||
else
|
||||
echo "❌ Google Authenticator command not found"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
@@ -44,21 +44,21 @@ function test_ssh_2fa_config() {
|
||||
echo "✅ ChallengeResponseAuthentication enabled"
|
||||
else
|
||||
echo "❌ ChallengeResponseAuthentication not enabled"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
if grep -q "^UsePAM yes" "$ssh_config"; then
|
||||
echo "✅ UsePAM enabled"
|
||||
else
|
||||
echo "❌ UsePAM not enabled"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
if grep -q "^AuthenticationMethods publickey,keyboard-interactive" "$ssh_config"; then
|
||||
echo "✅ AuthenticationMethods configured for 2FA"
|
||||
else
|
||||
echo "❌ AuthenticationMethods not configured for 2FA"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
@@ -75,7 +75,7 @@ function test_pam_2fa_config() {
|
||||
echo "✅ PAM Google Authenticator module configured"
|
||||
else
|
||||
echo "❌ PAM Google Authenticator module not configured"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Check if nullok is present (allows users without 2FA setup)
|
||||
@@ -106,7 +106,7 @@ function test_cockpit_2fa_config() {
|
||||
echo "✅ Cockpit configuration file exists"
|
||||
else
|
||||
echo "❌ Cockpit configuration file missing"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Check Cockpit PAM configuration
|
||||
@@ -114,7 +114,7 @@ function test_cockpit_2fa_config() {
|
||||
echo "✅ Cockpit PAM 2FA configured"
|
||||
else
|
||||
echo "❌ Cockpit PAM 2FA not configured"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
@@ -137,14 +137,14 @@ function test_webmin_2fa_config() {
|
||||
echo "✅ Webmin TOTP provider configured"
|
||||
else
|
||||
echo "❌ Webmin TOTP provider not configured"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
if grep -q "^twofactor=1" "$webmin_config"; then
|
||||
echo "✅ Webmin 2FA enabled"
|
||||
else
|
||||
echo "❌ Webmin 2FA not enabled"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
@@ -158,20 +158,22 @@ function test_user_2fa_setup() {
|
||||
|
||||
for user in "${users[@]}"; do
|
||||
if id "$user" &>/dev/null; then
|
||||
local user_home; user_home="$(getent passwd "$user" | cut -d: -f6)"
|
||||
|
||||
# Check if setup script exists
|
||||
if [[ -f "/tmp/setup-2fa-$user.sh" ]]; then
|
||||
echo "✅ 2FA setup script exists for user: $user"
|
||||
else
|
||||
echo "❌ 2FA setup script missing for user: $user"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Check if instructions exist
|
||||
if [[ -f "/home/$user/2fa-setup-instructions.txt" ]]; then
|
||||
if [[ -n "$user_home" && -f "$user_home/2fa-setup-instructions.txt" ]]; then
|
||||
echo "✅ 2FA instructions exist for user: $user"
|
||||
else
|
||||
echo "❌ 2FA instructions missing for user: $user"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
else
|
||||
echo "⚠️ User $user not found, skipping"
|
||||
@@ -191,7 +193,7 @@ function test_service_status() {
|
||||
echo "✅ SSH service is running"
|
||||
else
|
||||
echo "❌ SSH service is not running"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Test SSH configuration
|
||||
@@ -199,7 +201,7 @@ function test_service_status() {
|
||||
echo "✅ SSH configuration is valid"
|
||||
else
|
||||
echo "❌ SSH configuration is invalid"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Test Cockpit service if installed
|
||||
@@ -208,7 +210,7 @@ function test_service_status() {
|
||||
echo "✅ Cockpit service is running"
|
||||
else
|
||||
echo "❌ Cockpit service is not running"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -218,7 +220,7 @@ function test_service_status() {
|
||||
echo "✅ Webmin service is running"
|
||||
else
|
||||
echo "❌ Webmin service is not running"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -242,7 +244,7 @@ function test_backup_existence() {
|
||||
fi
|
||||
else
|
||||
echo "❌ Backup directory does not exist"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
|
||||
@@ -5,23 +5,26 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
FRAMEWORK_INCLUDES="$PROJECT_ROOT/vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/Framework-Includes"
|
||||
|
||||
function test_no_http_urls() {
|
||||
echo "🔍 Checking for HTTP URLs in scripts..."
|
||||
|
||||
local http_violations=0
|
||||
local script_dirs=("ProjectCode" "Framework-Includes" "Project-Includes")
|
||||
|
||||
local script_dirs=("$PROJECT_ROOT/ProjectCode" "$FRAMEWORK_INCLUDES" "$PROJECT_ROOT/Project-Includes")
|
||||
|
||||
for dir in "${script_dirs[@]}"; do
|
||||
if [[ -d "$PROJECT_ROOT/$dir" ]]; then
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Find HTTP URLs in shell scripts (excluding comments)
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -n "http://" "$file" | grep -v "^[[:space:]]*#" | grep -v "schema.org" | grep -v "xmlns"; then
|
||||
# grep -n prefixes "linenum:", so the comment filter must allow
|
||||
# for that prefix before the leading '#' of a comment line.
|
||||
if grep -n "http://" "$file" | grep -vE '^[0-9]+:[[:space:]]*#' | grep -v "schema.org" | grep -v "xmlns"; then
|
||||
echo "❌ HTTP URL found in: $file"
|
||||
((http_violations++))
|
||||
((++http_violations))
|
||||
fi
|
||||
done < <(find "$PROJECT_ROOT/$dir" -name "*.sh" -type f -print0)
|
||||
done < <(find "$dir" -name "*.sh" -type f -print0)
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -37,12 +40,12 @@ function test_no_http_urls() {
|
||||
function test_https_urls_valid() {
|
||||
echo "🔍 Validating HTTPS URLs are accessible..."
|
||||
|
||||
local script_dirs=("ProjectCode" "Framework-Includes" "Project-Includes")
|
||||
local script_dirs=("$PROJECT_ROOT/ProjectCode" "$FRAMEWORK_INCLUDES" "$PROJECT_ROOT/Project-Includes")
|
||||
local https_failures=0
|
||||
|
||||
|
||||
# Extract HTTPS URLs from scripts
|
||||
for dir in "${script_dirs[@]}"; do
|
||||
if [[ -d "$PROJECT_ROOT/$dir" ]]; then
|
||||
if [[ -d "$dir" ]]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
# Extract HTTPS URLs from non-comment lines
|
||||
grep -o "https://[^[:space:]\"']*" "$file" | grep -v "schema.org" | while read -r url; do
|
||||
@@ -51,10 +54,10 @@ function test_https_urls_valid() {
|
||||
echo "✅ HTTPS URL accessible: $url"
|
||||
else
|
||||
echo "❌ HTTPS URL not accessible: $url"
|
||||
((https_failures++))
|
||||
((++https_failures))
|
||||
fi
|
||||
done
|
||||
done < <(find "$PROJECT_ROOT/$dir" -name "*.sh" -type f -print0)
|
||||
done < <(find "$dir" -name "*.sh" -type f -print0)
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -73,12 +76,14 @@ function test_ssl_certificate_validation() {
|
||||
local ssl_failures=0
|
||||
|
||||
for url in "${test_urls[@]}"; do
|
||||
# Test with strict SSL verification
|
||||
if curl -s --fail --ssl-reqd --cert-status "$url" >/dev/null 2>&1; then
|
||||
# Verify TLS is required and the certificate chain is valid. Do NOT use
|
||||
# --cert-status: that requires OCSP stapling, which many valid CDNs do
|
||||
# not provide, producing false negatives for otherwise-valid certs.
|
||||
if curl -s --fail --ssl-reqd "$url" >/dev/null 2>&1; then
|
||||
echo "✅ SSL certificate valid: $url"
|
||||
else
|
||||
echo "❌ SSL certificate validation failed: $url"
|
||||
((ssl_failures++))
|
||||
((++ssl_failures))
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -5,37 +5,31 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Source framework functions
|
||||
source "$PROJECT_ROOT/Framework-Includes/Logging.sh" 2>/dev/null || echo "Warning: Logging.sh not found"
|
||||
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh" 2>/dev/null || echo "Warning: PrettyPrint.sh not found"
|
||||
source "$PROJECT_ROOT/Framework-Includes/ErrorHandling.sh" 2>/dev/null || echo "Warning: ErrorHandling.sh not found"
|
||||
# Source framework functions from the vendored KNELShellFramework
|
||||
FRAMEWORK_INCLUDES="$PROJECT_ROOT/vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/Framework-Includes"
|
||||
source "$FRAMEWORK_INCLUDES/Logging.sh" 2>/dev/null || echo "Warning: Logging.sh not found"
|
||||
source "$FRAMEWORK_INCLUDES/PrettyPrint.sh" 2>/dev/null || echo "Warning: PrettyPrint.sh not found"
|
||||
source "$FRAMEWORK_INCLUDES/ErrorHandling.sh" 2>/dev/null || echo "Warning: ErrorHandling.sh not found"
|
||||
|
||||
function test_logging_functions() {
|
||||
echo "🔍 Testing logging functions..."
|
||||
|
||||
local test_log="/tmp/test-log-$$"
|
||||
|
||||
# Test if logging functions exist and work
|
||||
if command -v log_info >/dev/null 2>&1; then
|
||||
log_info "Test info message" 2>/dev/null || true
|
||||
echo "✅ log_info function exists"
|
||||
function test_logging_variables() {
|
||||
echo "🔍 Testing logging variables..."
|
||||
|
||||
if [[ -n "${CURRENT_TIMESTAMP:-}" ]]; then
|
||||
echo "✅ CURRENT_TIMESTAMP is set"
|
||||
else
|
||||
echo "❌ log_info function missing"
|
||||
echo "❌ CURRENT_TIMESTAMP is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v log_error >/dev/null 2>&1; then
|
||||
log_error "Test error message" 2>/dev/null || true
|
||||
echo "✅ log_error function exists"
|
||||
|
||||
if [[ -n "${LOGFILENAME:-}" ]]; then
|
||||
echo "✅ LOGFILENAME is set"
|
||||
else
|
||||
echo "❌ log_error function missing"
|
||||
echo "❌ LOGFILENAME is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$test_log"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -59,14 +53,6 @@ function test_pretty_print_functions() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v print_success >/dev/null 2>&1; then
|
||||
print_success "Test success message" >/dev/null 2>&1 || true
|
||||
echo "✅ print_success function exists"
|
||||
else
|
||||
echo "❌ print_success function missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -74,10 +60,17 @@ function test_error_handling() {
|
||||
echo "🔍 Testing error handling..."
|
||||
|
||||
# Test if error handling functions exist
|
||||
if command -v handle_error >/dev/null 2>&1; then
|
||||
echo "✅ handle_error function exists"
|
||||
if command -v error_out >/dev/null 2>&1; then
|
||||
echo "✅ error_out function exists"
|
||||
else
|
||||
echo "❌ handle_error function missing"
|
||||
echo "❌ error_out function missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v handle_failure >/dev/null 2>&1; then
|
||||
echo "✅ handle_failure function exists"
|
||||
else
|
||||
echo "❌ handle_failure function missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -112,11 +105,11 @@ function test_framework_includes_exist() {
|
||||
local missing_files=0
|
||||
|
||||
for include_file in "${required_includes[@]}"; do
|
||||
if [[ -f "$PROJECT_ROOT/Framework-Includes/$include_file" ]]; then
|
||||
if [[ -f "$FRAMEWORK_INCLUDES/$include_file" ]]; then
|
||||
echo "✅ Framework include exists: $include_file"
|
||||
else
|
||||
echo "❌ Framework include missing: $include_file"
|
||||
((missing_files++))
|
||||
((++missing_files))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -127,18 +120,28 @@ function test_syntax_validation() {
|
||||
echo "🔍 Testing script syntax validation..."
|
||||
|
||||
local syntax_errors=0
|
||||
local script_dirs=("Framework-Includes" "Project-Includes" "ProjectCode")
|
||||
|
||||
local script_dirs=(
|
||||
"$FRAMEWORK_INCLUDES"
|
||||
"$PROJECT_ROOT/Project-Includes"
|
||||
"$PROJECT_ROOT/ProjectCode"
|
||||
)
|
||||
|
||||
for dir in "${script_dirs[@]}"; do
|
||||
if [[ -d "$PROJECT_ROOT/$dir" ]]; then
|
||||
if [[ -d "$dir" ]]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
# Skip files that aren't bash scripts despite a .sh extension (e.g. PHP agents)
|
||||
local shebang
|
||||
shebang="$(head -c 32 "$file" 2>/dev/null)"
|
||||
case "$shebang" in
|
||||
*php*|*python*|*perl*) continue ;;
|
||||
esac
|
||||
if bash -n "$file" 2>/dev/null; then
|
||||
echo "✅ Syntax valid: $(basename "$file")"
|
||||
else
|
||||
echo "❌ Syntax error in: $(basename "$file")"
|
||||
((syntax_errors++))
|
||||
((++syntax_errors))
|
||||
fi
|
||||
done < <(find "$PROJECT_ROOT/$dir" -name "*.sh" -type f -print0)
|
||||
done < <(find "$dir" -name "*.sh" -type f -print0)
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -154,7 +157,7 @@ function main() {
|
||||
|
||||
# Run all unit tests
|
||||
test_framework_includes_exist || ((total_failures++))
|
||||
test_logging_functions || ((total_failures++))
|
||||
test_logging_variables || ((total_failures++))
|
||||
test_pretty_print_functions || ((total_failures++))
|
||||
test_error_handling || ((total_failures++))
|
||||
test_syntax_validation || ((total_failures++))
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Source framework functions
|
||||
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
|
||||
# Source framework functions from the vendored KNELShellFramework
|
||||
FRAMEWORK_INCLUDES="$PROJECT_ROOT/vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/Framework-Includes"
|
||||
|
||||
# The vendored PrettyPrint only defines print_info/print_error, but SafeDownload.sh
|
||||
# calls print_success/print_warning; define lightweight shims before sourcing.
|
||||
function print_success() { echo "✅ $1"; }
|
||||
function print_warning() { echo "⚠️ $1"; }
|
||||
|
||||
source "$FRAMEWORK_INCLUDES/SafeDownload.sh"
|
||||
|
||||
function test_network_connectivity() {
|
||||
echo "🔍 Testing network connectivity..."
|
||||
|
||||
if test_network_connectivity; then
|
||||
|
||||
if check_url_accessibility "https://github.com"; then
|
||||
echo "✅ Network connectivity test passed"
|
||||
return 0
|
||||
else
|
||||
@@ -37,7 +44,7 @@ function test_url_accessibility() {
|
||||
echo "✅ URL accessible: $url"
|
||||
else
|
||||
echo "❌ URL not accessible: $url"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -60,20 +67,20 @@ function test_safe_download() {
|
||||
echo "✅ Downloaded file exists and has content"
|
||||
else
|
||||
echo "❌ Downloaded file is missing or empty"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$test_dest"
|
||||
else
|
||||
echo "❌ Safe download failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Test download with invalid URL
|
||||
if safe_download "https://invalid.example.com/nonexistent" "/tmp/test-invalid-$$" 2>/dev/null; then
|
||||
echo "❌ Invalid URL download should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Invalid URL download failed as expected"
|
||||
fi
|
||||
@@ -97,13 +104,13 @@ function test_checksum_verification() {
|
||||
echo "✅ Correct checksum verification passed"
|
||||
else
|
||||
echo "❌ Correct checksum verification failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
# Test incorrect checksum
|
||||
if verify_checksum "$test_file" "invalid_checksum" 2>/dev/null; then
|
||||
echo "❌ Incorrect checksum should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Incorrect checksum verification failed as expected"
|
||||
fi
|
||||
@@ -111,7 +118,7 @@ function test_checksum_verification() {
|
||||
# Test missing file
|
||||
if verify_checksum "/tmp/nonexistent-file-$$" "$expected_checksum" 2>/dev/null; then
|
||||
echo "❌ Missing file checksum should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Missing file checksum verification failed as expected"
|
||||
fi
|
||||
@@ -143,7 +150,7 @@ function test_batch_download() {
|
||||
echo "✅ Batch file downloaded: $(basename "$file")"
|
||||
else
|
||||
echo "❌ Batch file missing: $(basename "$file")"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -153,7 +160,7 @@ function test_batch_download() {
|
||||
done
|
||||
else
|
||||
echo "❌ Batch download failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
@@ -172,7 +179,7 @@ function test_config_backup_and_restore() {
|
||||
# Test safe config download (this will fail with invalid URL, triggering restore)
|
||||
if safe_config_download "https://invalid.example.com/config" "$test_config" ".test-backup" 2>/dev/null; then
|
||||
echo "❌ Invalid config download should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Invalid config download failed as expected"
|
||||
|
||||
@@ -181,7 +188,7 @@ function test_config_backup_and_restore() {
|
||||
echo "✅ Original config was restored after failed download"
|
||||
else
|
||||
echo "❌ Original config was not restored properly"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -199,22 +206,26 @@ function test_download_error_handling() {
|
||||
# Test download with missing parameters
|
||||
if safe_download "" "/tmp/test" 2>/dev/null; then
|
||||
echo "❌ Download with empty URL should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Download with empty URL failed as expected"
|
||||
fi
|
||||
|
||||
if safe_download "https://example.com" "" 2>/dev/null; then
|
||||
echo "❌ Download with empty destination should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Download with empty destination failed as expected"
|
||||
fi
|
||||
|
||||
# Test download to read-only location (should fail)
|
||||
if safe_download "https://github.com" "/test-readonly-$$" 2>/dev/null; then
|
||||
# Test download to read-only location (should fail). Only meaningful for
|
||||
# non-root users: root bypasses filesystem permissions, so the expected
|
||||
# write failure never happens and the assertion is invalid.
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo "⏭️ Skipping read-only-location test (running as root; root bypasses FS perms)"
|
||||
elif safe_download "https://github.com" "/test-readonly-$$" 2>/dev/null; then
|
||||
echo "❌ Download to read-only location should have failed"
|
||||
((failed++))
|
||||
((++failed))
|
||||
else
|
||||
echo "✅ Download to read-only location failed as expected"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Redundant DNS/NTP Validation Test
|
||||
# Validates that the host is configured to use the redundant pfv-netinfra-01/02
|
||||
# pair for name resolution and time, and that both servers actually answer.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# The authoritative pair (pfv-netinfra-01 / pfv-netinfra-02).
|
||||
DNS_PRIMARY="192.168.3.252"
|
||||
DNS_SECONDARY="192.168.3.253"
|
||||
NTP_PRIMARY="192.168.3.252"
|
||||
NTP_SECONDARY="192.168.3.253"
|
||||
|
||||
RESOLV_CONF="/etc/resolv.conf"
|
||||
NTP_CONF="/etc/ntpsec/ntp.conf"
|
||||
|
||||
# A name every recursive resolver must be able to resolve.
|
||||
DNS_PROBE_NAME="github.com"
|
||||
|
||||
failed=0
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# --- Configuration assertions -------------------------------------------------
|
||||
|
||||
function test_dns_config_present() {
|
||||
echo "🔍 Checking $RESOLV_CONF ..."
|
||||
local problems=0
|
||||
|
||||
if [[ -L "$RESOLV_CONF" ]]; then
|
||||
echo "❌ $RESOLV_CONF is a symlink (would be overwritten by a resolver manager)"
|
||||
((++problems))
|
||||
elif [[ ! -f "$RESOLV_CONF" ]]; then
|
||||
echo "❌ $RESOLV_CONF missing"
|
||||
((++problems))
|
||||
fi
|
||||
|
||||
for ns in "$DNS_PRIMARY" "$DNS_SECONDARY"; do
|
||||
if grep -Eq "^[[:space:]]*nameserver[[:space:]]+$ns" "$RESOLV_CONF" 2>/dev/null; then
|
||||
echo "✅ nameserver $ns configured"
|
||||
else
|
||||
echo "❌ nameserver $ns NOT in $RESOLV_CONF"
|
||||
((++problems))
|
||||
fi
|
||||
done
|
||||
|
||||
return $problems
|
||||
}
|
||||
|
||||
function test_ntp_config_present() {
|
||||
echo "🔍 Checking $NTP_CONF ..."
|
||||
if [[ ! -f "$NTP_CONF" ]]; then
|
||||
echo "❌ $NTP_CONF missing (is ntpsec installed?)"
|
||||
return 1
|
||||
fi
|
||||
local problems=0
|
||||
for s in "$NTP_PRIMARY" "$NTP_SECONDARY"; do
|
||||
if grep -Eq "^[[:space:]]*(server|pool)[[:space:]]+$s" "$NTP_CONF"; then
|
||||
echo "✅ NTP server $s configured"
|
||||
else
|
||||
echo "❌ NTP server $s NOT in $NTP_CONF"
|
||||
((++problems))
|
||||
fi
|
||||
done
|
||||
return $problems
|
||||
}
|
||||
|
||||
# --- Functional assertions: each server actually answers ----------------------
|
||||
|
||||
function _dns_resolves() {
|
||||
# $1 = server ip. Returns 0 if it resolves DNS_PROBE_NAME.
|
||||
local server="$1"
|
||||
if have dig; then
|
||||
dig @"$server" +short +time=4 +tries=1 "$DNS_PROBE_NAME" A 2>/dev/null | grep -Eq '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'
|
||||
elif have nslookup; then
|
||||
nslookup "$DNS_PROBE_NAME" "$server" 2>/dev/null | grep -Eq 'Address:[[:space:]]*[0-9]'
|
||||
elif have host; then
|
||||
host "$DNS_PROBE_NAME" "$server" 2>/dev/null | grep -Eq 'has address'
|
||||
else
|
||||
# Last resort: the resolver itself.
|
||||
getent ahostsv4 "$DNS_PROBE_NAME" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
function test_dns_servers_answer() {
|
||||
echo "🔍 Probing DNS servers ..."
|
||||
local problems=0
|
||||
for ns in "$DNS_PRIMARY" "$DNS_SECONDARY"; do
|
||||
if _dns_resolves "$ns"; then
|
||||
echo "✅ $ns resolves $DNS_PROBE_NAME"
|
||||
else
|
||||
echo "❌ $ns did not resolve $DNS_PROBE_NAME"
|
||||
((++problems))
|
||||
fi
|
||||
done
|
||||
return $problems
|
||||
}
|
||||
|
||||
function _ntp_answers() {
|
||||
# $1 = server ip. Returns 0 if it responds to a time query.
|
||||
local server="$1"
|
||||
if have ntpdate; then
|
||||
timeout 8 ntpdate -q "$server" 2>/dev/null | grep -Eq 'no-leap|leap'
|
||||
elif have sntp; then
|
||||
timeout 8 sntp -t 4 "$server" >/dev/null 2>&1
|
||||
elif have chronyc; then
|
||||
# NTS/chrony not expected here, but be tolerant.
|
||||
chronyc -n -h "$server" tracking >/dev/null 2>&1
|
||||
else
|
||||
return 2 # cannot test
|
||||
fi
|
||||
}
|
||||
|
||||
function test_ntp_servers_answer() {
|
||||
echo "🔍 Probing NTP servers ..."
|
||||
local problems=0
|
||||
for s in "$NTP_PRIMARY" "$NTP_SECONDARY"; do
|
||||
if _ntp_answers "$s"; then
|
||||
echo "✅ NTP $s responds to time query"
|
||||
else
|
||||
echo "❌ $s did not respond to NTP query"
|
||||
((++problems))
|
||||
fi
|
||||
done
|
||||
return $problems
|
||||
}
|
||||
|
||||
# --- End-to-end: the host is actually USING the pair --------------------------
|
||||
|
||||
function test_resolver_endtoend() {
|
||||
echo "🔍 End-to-end resolution via $RESOLV_CONF ..."
|
||||
if getent ahostsv4 "$DNS_PROBE_NAME" >/dev/null 2>&1; then
|
||||
echo "✅ Host resolves $DNS_PROBE_NAME via configured resolver"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Host cannot resolve $DNS_PROBE_NAME via configured resolver"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function test_ntp_daemon_peers() {
|
||||
echo "🔍 NTP daemon peer list ..."
|
||||
local peers
|
||||
if have ntpq; then
|
||||
peers="$(ntpq -pn 2>/dev/null || true)"
|
||||
elif have chronyc; then
|
||||
peers="$(chronyc -n sources 2>/dev/null || true)"
|
||||
else
|
||||
echo "⚠️ No ntpq/chronyc available; skipping daemon peer check"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local problems=0
|
||||
for s in "$NTP_PRIMARY" "$NTP_SECONDARY"; do
|
||||
if echo "$peers" | grep -Eq "^\\s*${s//./\\.}"; then
|
||||
echo "✅ NTP daemon has peer $s"
|
||||
else
|
||||
echo "❌ NTP daemon is NOT tracking $s"
|
||||
((++problems))
|
||||
fi
|
||||
done
|
||||
|
||||
# Sync status is informational only: a freshly started daemon needs several
|
||||
# polls before the reach counter stabilises, so we warn rather than fail.
|
||||
if echo "$peers" | grep -Eq '\*'; then
|
||||
echo "✅ NTP daemon reports a synced peer"
|
||||
else
|
||||
echo "⚠️ NTP daemon not yet synced (normal for a few minutes after restart)"
|
||||
fi
|
||||
return $problems
|
||||
}
|
||||
|
||||
# --- Main ---------------------------------------------------------------------
|
||||
|
||||
function main() {
|
||||
echo "🛰️ Running Redundant DNS/NTP Validation Tests"
|
||||
echo "================================================"
|
||||
|
||||
local total_failures=0
|
||||
|
||||
test_dns_config_present || ((++total_failures))
|
||||
test_ntp_config_present || ((++total_failures))
|
||||
test_dns_servers_answer || ((++total_failures))
|
||||
test_ntp_servers_answer || ((++total_failures))
|
||||
test_resolver_endtoend || ((++total_failures))
|
||||
test_ntp_daemon_peers || ((++total_failures))
|
||||
|
||||
echo "================================================"
|
||||
if [[ $total_failures -eq 0 ]]; then
|
||||
echo "✅ All redundant DNS/NTP validation tests passed"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ $total_failures redundant DNS/NTP tests failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -44,7 +44,7 @@ function test_required_commands() {
|
||||
echo "✅ Required command available: $cmd"
|
||||
else
|
||||
echo "❌ Required command missing: $cmd"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -87,7 +87,7 @@ function test_network_connectivity() {
|
||||
echo "✅ Network connectivity: $url"
|
||||
else
|
||||
echo "❌ Network connectivity failed: $url"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -103,7 +103,7 @@ function test_permissions() {
|
||||
echo "✅ Write permission: $dir"
|
||||
else
|
||||
echo "❌ Write permission denied: $dir"
|
||||
((failed++))
|
||||
((++failed))
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
Executable
+330
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# vm-validation.sh
|
||||
#
|
||||
# End-to-end validation driver for KNELServerBuild on a sandbox VM.
|
||||
#
|
||||
# This script drives a Proxmox VM through: snapshot -> deploy -> validate, with
|
||||
# one-command rollback. It is designed to be re-run after code fixes are pushed.
|
||||
#
|
||||
# DESIGN: deployment is GIT-BASED. The VM clones (or pulls) the public repo
|
||||
# itself, exactly as a real fresh server would — so the result is identical no
|
||||
# matter who runs this script (no reliance on a local working copy or rsync).
|
||||
# All SSH/SCP access goes through Project-Tests/remote.sh; never call ssh here.
|
||||
#
|
||||
# USAGE:
|
||||
# # Discover the numeric VMID on Proxmox:
|
||||
# ./Project-Tests/vm-validation.sh find-vmid
|
||||
#
|
||||
# # Full loop (snapshot + deploy + validate), auto-rollback on failure:
|
||||
# VM_ID=6000 ./Project-Tests/vm-validation.sh all
|
||||
#
|
||||
# # Individual steps:
|
||||
# VM_ID=6000 ./Project-Tests/vm-validation.sh snapshot
|
||||
# VM_ID=6000 ./Project-Tests/vm-validation.sh deploy
|
||||
# VM_ID=6000 ./Project-Tests/vm-validation.sh validate
|
||||
# VM_ID=6000 ./Project-Tests/vm-validation.sh rollback [snapshot-name]
|
||||
#
|
||||
# # Clean re-deploy from scratch (delete + re-clone on VM):
|
||||
# VM_ID=6000 CLEAN_CLONE=1 ./Project-Tests/vm-validation.sh deploy
|
||||
#
|
||||
# CONFIG (override via env, all have sensible defaults):
|
||||
# PROX_HOST Proxmox node hostname (default: pfv-tsys5)
|
||||
# PROX_USER SSH user on Proxmox (default: root)
|
||||
# VM_NAME VM name for VMID lookup/logging (default: sectestbed-sandbox)
|
||||
# VM_IP VM IP for SSH (default: 192.168.3.50)
|
||||
# VM_USER SSH user on the VM (default: localuser)
|
||||
# VM_ID Numeric VMID on Proxmox (REQUIRED except for find-vmid)
|
||||
# REPO_URL git URL the VM clones (default: https://git.knownelement.com/KNEL/KNELServerBuild.git)
|
||||
# REMOTE_REPO clone dir under ~$VM_USER (default: KNELServerBuild)
|
||||
# SNAP_PREFIX snapshot name prefix (default: pre-knel-deploy)
|
||||
# CLEAN_CLONE if set, delete + re-clone on VM (default: unset)
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
PROX_HOST="${PROX_HOST:-pfv-tsys5}"
|
||||
PROX_USER="${PROX_USER:-root}"
|
||||
VM_NAME="${VM_NAME:-sectestbed-sandbox}"
|
||||
VM_IP="${VM_IP:-192.168.3.50}"
|
||||
VM_USER="${VM_USER:-localuser}"
|
||||
VM_ID="${VM_ID:-}"
|
||||
REPO_URL="${REPO_URL:-https://git.knownelement.com/KNEL/KNELServerBuild.git}"
|
||||
REMOTE_REPO="${REMOTE_REPO:-KNELServerBuild}"
|
||||
SNAP_PREFIX="${SNAP_PREFIX:-pre-knel-deploy}"
|
||||
ACCESS_PUBKEY="${ACCESS_PUBKEY:-$HOME/.ssh/id_ed25519.pub}"
|
||||
# Re-inject the validation pubkey after each deploy (secharden-ssh replaces
|
||||
# authorized_keys with the managed production key set, locking out the
|
||||
# bootstrap/dev key). Set RESTORE_ACCESS=0 to disable.
|
||||
RESTORE_ACCESS="${RESTORE_ACCESS:-1}"
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_SRC="$(cd "$HERE/.." && pwd)"
|
||||
REMOTE="$HERE/remote.sh"
|
||||
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
SNAP_NAME="${SNAP_PREFIX}-${STAMP}"
|
||||
LOCAL_LOG_DIR="$REPO_SRC/logs/vm-validation"
|
||||
mkdir -p "$LOCAL_LOG_DIR"
|
||||
LOCAL_LOG="$LOCAL_LOG_DIR/run-${STAMP}.log"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" | tee -a "$LOCAL_LOG"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
# All remote access funnels through remote.sh.
|
||||
vm() { bash "$REMOTE" vm "$@"; } # as $VM_USER (SSH)
|
||||
vmroot() { bash "$REMOTE" vmroot "$@"; } # as root via sudo (SSH)
|
||||
vmfile() { bash "$REMOTE" vm-file "$@"; } # run local script on VM (SSH)
|
||||
vmguest() { bash "$REMOTE" vm-guest "$@"; } # as root via guest agent (no SSH/2FA)
|
||||
prox() { bash "$REMOTE" prox "$@"; } # as $PROX_USER on Proxmox
|
||||
|
||||
require_vm_id() {
|
||||
[[ -n "$VM_ID" ]] || die "VM_ID is required for this command. Find it with: $0 find-vmid"
|
||||
}
|
||||
|
||||
wait_for_vm_ssh() {
|
||||
log "Waiting for SSH on ${VM_USER}@${VM_IP} to come up..."
|
||||
for i in $(seq 1 60); do
|
||||
if vm 'true' >/dev/null 2>&1; then
|
||||
log "SSH is up (after ${i} tries)."
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
die "VM did not become SSH-reachable within 5 minutes."
|
||||
}
|
||||
|
||||
# Resolve the ABSOLUTE path of the repo clone on the VM (as $VM_USER) and echo
|
||||
# it. Using an absolute path avoids the '~' -> root's home trap under sudo.
|
||||
resolve_remote_repo() {
|
||||
local p
|
||||
p="$(vm "cd ~/${REMOTE_REPO} 2>/dev/null && pwd" 2>/dev/null)"
|
||||
[[ -n "$p" ]] || p="$(vmguest "cd ~${VM_USER}/${REMOTE_REPO} 2>/dev/null && pwd" 2>/dev/null)"
|
||||
printf '%s' "$p"
|
||||
}
|
||||
|
||||
# Re-inject the validation pubkey into ~$VM_USER/.ssh/authorized_keys OUT OF
|
||||
# BAND via the Proxmox guest agent (qm guest exec runs as root inside the VM
|
||||
# and does not depend on SSH). This is necessary because secharden-ssh replaces
|
||||
# authorized_keys with the managed production key set, which would otherwise
|
||||
# lock out the bootstrap key used to drive validation. No-op if SSH still works.
|
||||
restore_vm_access() {
|
||||
[[ "$RESTORE_ACCESS" = "1" ]] || { log "RESTORE_ACCESS=0; skipping access restore."; return 0; }
|
||||
[[ -f "$ACCESS_PUBKEY" ]] || { log "WARN: ACCESS_PUBKEY not found ($ACCESS_PUBKEY); cannot restore access."; return 0; }
|
||||
if vm 'true' >/dev/null 2>&1; then
|
||||
log "SSH access already works; no need to restore."
|
||||
return 0
|
||||
fi
|
||||
log "SSH access lost (expected after secharden-ssh). Restoring via Proxmox guest agent..."
|
||||
local payload_b64
|
||||
# Leading newline guards against the managed authorized_keys lacking a
|
||||
# trailing newline (which would otherwise concatenate two keys into one).
|
||||
payload_b64="$(printf '\n%s' "$(cat "$ACCESS_PUBKEY")" | base64 -w0)"
|
||||
prox "qm guest exec $VM_ID -- /bin/sh -c 'echo $payload_b64 | base64 -d >> /home/${VM_USER}/.ssh/authorized_keys'" \
|
||||
>/dev/null 2>&1 || { log "WARN: guest-agent key append failed."; return 0; }
|
||||
prox "qm guest exec $VM_ID -- /bin/sh -c 'chown ${VM_USER}:${VM_USER} /home/${VM_USER}/.ssh/authorized_keys; chmod 600 /home/${VM_USER}/.ssh/authorized_keys'" \
|
||||
>/dev/null 2>&1 || true
|
||||
if vm 'true' >/dev/null 2>&1; then
|
||||
log "Access restored."
|
||||
return 0
|
||||
fi
|
||||
# If SSH still fails after re-injecting the key, 2FA is almost certainly the
|
||||
# cause (secharden-2fa enforces publickey+keyboard-interactive, which no
|
||||
# non-interactive SSH client can satisfy). That is expected and not fatal:
|
||||
# the guest agent still gives us full out-of-band access for log fetch and
|
||||
# the validation suite.
|
||||
if vmguest 'grep -q "^AuthenticationMethods" /etc/ssh/sshd_config' >/dev/null 2>&1; then
|
||||
log "SSH requires 2FA (expected after secharden-2fa); using guest agent for further access."
|
||||
else
|
||||
log "WARN: access still not working after restore and 2FA not detected. Check sshd_config."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
cmd_find_vmid() {
|
||||
log "Listing VMs on Proxmox host '$PROX_HOST' matching '$VM_NAME':"
|
||||
prox 'qm list' 2>&1 | tee -a "$LOCAL_LOG" \
|
||||
| { IFS= read -r header; echo "$header"; grep -i "$VM_NAME" || true; }
|
||||
log "Set VM_ID=<number> env var based on the row above."
|
||||
}
|
||||
|
||||
cmd_snapshot() {
|
||||
require_vm_id
|
||||
log "Creating snapshot '$SNAP_NAME' of VMID $VM_ID on $PROX_HOST..."
|
||||
prox "qm snapshot $VM_ID $SNAP_NAME --vmstate 1" 2>&1 | tee -a "$LOCAL_LOG" \
|
||||
|| die "Snapshot creation failed."
|
||||
echo "$SNAP_NAME" > "$LOCAL_LOG_DIR/.last-snapshot"
|
||||
log "Snapshot '$SNAP_NAME' recorded as rollback target."
|
||||
}
|
||||
|
||||
cmd_rollback() {
|
||||
require_vm_id
|
||||
local target="${1:-$(cat "$LOCAL_LOG_DIR/.last-snapshot" 2>/dev/null || true)}"
|
||||
[[ -n "$target" ]] || die "No snapshot name given and no .last-snapshot on disk."
|
||||
log "Rolling back VMID $VM_ID to snapshot '$target'..."
|
||||
# Proxmox rollback requires the VM to be stopped.
|
||||
prox "qm stop $VM_ID" 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
sleep 5
|
||||
prox "qm rollback $VM_ID $target" 2>&1 | tee -a "$LOCAL_LOG" \
|
||||
|| die "Rollback command failed."
|
||||
log "Starting VMID $VM_ID..."
|
||||
prox "qm start $VM_ID" 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
wait_for_vm_ssh
|
||||
log "Rollback complete."
|
||||
}
|
||||
|
||||
# Ensure the VM has git + ca-certificates (fresh-server bootstrap).
|
||||
bootstrap_git_on_vm() {
|
||||
log "Ensuring git is present on the VM..."
|
||||
vm 'command -v git >/dev/null 2>&1 || sudo -n DEBIAN_FRONTEND=noninteractive apt-get -y -qq install git ca-certificates' \
|
||||
2>&1 | tee -a "$LOCAL_LOG" || die "Failed to bootstrap git on VM."
|
||||
vm 'sudo -n DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates' 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
}
|
||||
|
||||
# Clone or pull the repo on the VM. Returns absolute path on stdout (via log).
|
||||
sync_repo_on_vm() {
|
||||
bootstrap_git_on_vm
|
||||
if [[ -n "${CLEAN_CLONE:-}" ]]; then
|
||||
log "CLEAN_CLONE set: removing existing clone on VM."
|
||||
vm "rm -rf ~/${REMOTE_REPO}" 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
fi
|
||||
log "Ensuring repo is cloned/pulled on the VM from:"
|
||||
log " $REPO_URL"
|
||||
vm "
|
||||
set -e
|
||||
if [ -d ~/${REMOTE_REPO}/.git ]; then
|
||||
cd ~/${REMOTE_REPO}
|
||||
git fetch --all --prune
|
||||
git reset --hard origin/HEAD 2>/dev/null || git reset --hard origin/main
|
||||
git clean -xfd
|
||||
else
|
||||
git clone --filter=blob:none '$REPO_URL' ~/${REMOTE_REPO}
|
||||
cd ~/${REMOTE_REPO}
|
||||
fi
|
||||
git log --oneline -1
|
||||
" 2>&1 | tee -a "$LOCAL_LOG" || die "Repo sync failed on VM."
|
||||
log "Repo ready on VM."
|
||||
}
|
||||
|
||||
# The remote setup runner: a self-contained script we ship to the VM so the
|
||||
# sudo'd setup runs from a known-good absolute path with full logging. Using a
|
||||
# file avoids nested-quote hell across local -> ssh -> sudo -> bash -c.
|
||||
deploy_runner_script() {
|
||||
cat <<RUNNER
|
||||
#!/usr/bin/bash
|
||||
# remote-setup-runner.sh (generated by vm-validation.sh)
|
||||
# Runs ProjectCode/SetupNewSystem.sh from the repo given by \$1, as root.
|
||||
set -uo pipefail
|
||||
# Ensure a sane TERM so the framework's tput-based color helpers work when run
|
||||
# over a non-interactive SSH session (which has no TTY/TERM by default).
|
||||
export TERM="\${TERM:-linux}"
|
||||
REPO_ABS="\${1:?repo abs path required}"
|
||||
REMOTE_LOG="/tmp/knel-setup.log"
|
||||
echo "=== KNEL SetupNewSystem start: \$(date -Is) repo=\$REPO_ABS ===" | tee -a "\$REMOTE_LOG"
|
||||
cd "\$REPO_ABS/ProjectCode" || { echo "FATAL: ProjectCode missing at \$REPO_ABS"; exit 2; }
|
||||
bash SetupNewSystem.sh 2>&1 | tee -a "\$REMOTE_LOG"
|
||||
rc=\${PIPESTATUS[0]}
|
||||
echo "=== KNEL SetupNewSystem end: rc=\$rc \$(date -Is) ===" | tee -a "\$REMOTE_LOG"
|
||||
exit \$rc
|
||||
RUNNER
|
||||
}
|
||||
|
||||
cmd_deploy() {
|
||||
require_vm_id
|
||||
sync_repo_on_vm
|
||||
local repo_abs
|
||||
repo_abs="$(resolve_remote_repo)"
|
||||
[[ -n "$repo_abs" ]] || die "Could not resolve absolute repo path on VM."
|
||||
log "Repo absolute path on VM: $repo_abs"
|
||||
|
||||
# Ship the runner script and execute it as root via sudo, passing abs path.
|
||||
local runner_local="$LOCAL_LOG_DIR/remote-setup-runner.sh"
|
||||
deploy_runner_script > "$runner_local"
|
||||
vm "mkdir -p ~/${REMOTE_REPO}/Project-Tests/.run" 2>&1 | tee -a "$LOCAL_LOG"
|
||||
bash "$REMOTE" vm-copy "$runner_local" "${REMOTE_REPO}/Project-Tests/.run/remote-setup-runner.sh" \
|
||||
2>&1 | tee -a "$LOCAL_LOG" || die "Failed to ship runner script."
|
||||
|
||||
log "Running SetupNewSystem.sh on the VM as root (this takes several minutes)..."
|
||||
# Resolve abs runner path the same way (no ~ under sudo).
|
||||
local runner_abs
|
||||
runner_abs="$(vm "cd ~/${REMOTE_REPO}/Project-Tests/.run && pwd")/remote-setup-runner.sh"
|
||||
vmroot "bash '$runner_abs' '$repo_abs'" 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
|
||||
# secharden-ssh (run near the end of setup) replaces authorized_keys with the
|
||||
# managed production key set, locking out the bootstrap key. Restore the
|
||||
# validation key out-of-band BEFORE we try to fetch the log over SSH.
|
||||
restore_vm_access
|
||||
|
||||
# Fetch the remote log for full fidelity (strip ANSI color codes). SSH works
|
||||
# only until secharden-2fa flips 2FA on; after that, use the guest agent.
|
||||
local fetch_cmd="sed -r 's/\\x1B\\[[0-9;]*[mK]//g' /tmp/knel-setup.log 2>/dev/null || cat /tmp/knel-setup.log"
|
||||
if ! vm "$fetch_cmd" > "$LOCAL_LOG_DIR/setup-output-${STAMP}.log" 2>/dev/null; then
|
||||
vmguest "$fetch_cmd" > "$LOCAL_LOG_DIR/setup-output-${STAMP}.log" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Detect the exit marker. Prefer the full fetched log, but always fall back
|
||||
# to the live stream ($LOCAL_LOG) which is captured regardless of whether
|
||||
# post-setup SSH/2FA let us fetch the remote log.
|
||||
local rc_marker
|
||||
rc_marker=$(grep -oE 'rc=[0-9]+' "$LOCAL_LOG_DIR/setup-output-${STAMP}.log" 2>/dev/null | tail -1 || true)
|
||||
[[ -n "$rc_marker" ]] || rc_marker=$(grep -oE 'rc=[0-9]+' "$LOCAL_LOG" 2>/dev/null | tail -1 || true)
|
||||
log "Setup run finished. Marker: ${rc_marker:-unknown}"
|
||||
|
||||
if [[ "${rc_marker:-}" != "rc=0" ]]; then
|
||||
log "Setup did NOT complete cleanly. See: $LOCAL_LOG_DIR/setup-output-${STAMP}.log (and $LOCAL_LOG)"
|
||||
return 1
|
||||
fi
|
||||
log "Setup completed successfully."
|
||||
}
|
||||
|
||||
cmd_validate() {
|
||||
require_vm_id
|
||||
log "Running post-deploy validation suite on the VM..."
|
||||
local repo_abs
|
||||
repo_abs="$(resolve_remote_repo)"
|
||||
[[ -n "$repo_abs" ]] || die "Could not resolve absolute repo path on VM."
|
||||
# Prefer SSH; fall back to the guest agent (post-2FA SSH needs a TOTP token).
|
||||
if ! vmroot "cd '$repo_abs' && bash Project-Tests/run-tests.sh all" 2>&1 | tee -a "$LOCAL_LOG"; then
|
||||
vmguest "cd '$repo_abs' && bash Project-Tests/run-tests.sh all" 2>&1 | tee -a "$LOCAL_LOG" || true
|
||||
fi
|
||||
log "Validation run finished. Inspect output above / in $LOCAL_LOG."
|
||||
}
|
||||
|
||||
cmd_all() {
|
||||
require_vm_id
|
||||
log "=== FULL VALIDATION LOOP: $VM_NAME (VMID $VM_ID) ==="
|
||||
cmd_snapshot
|
||||
if cmd_deploy && cmd_validate; then
|
||||
log "=== ALL GREEN ==="
|
||||
return 0
|
||||
fi
|
||||
log "=== FAILURE — auto-rolling back to '$SNAP_NAME' ==="
|
||||
cmd_rollback "$SNAP_NAME"
|
||||
log "Rolled back. Fix and push, then re-run: VM_ID=$VM_ID $0 deploy && VM_ID=$VM_ID $0 validate"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
subcmd="${1:-}"
|
||||
case "$subcmd" in
|
||||
find-vmid) cmd_find_vmid ;;
|
||||
snapshot) cmd_snapshot ;;
|
||||
deploy) cmd_deploy ;;
|
||||
validate) cmd_validate ;;
|
||||
rollback) cmd_rollback "${2:-}" ;;
|
||||
all) cmd_all ;;
|
||||
""|-h|--help|help)
|
||||
sed -n '2,49p' "${BASH_SOURCE[0]}" >&2
|
||||
exit 0
|
||||
;;
|
||||
*) die "Unknown command '$subcmd'. Run '$0 help'." ;;
|
||||
esac
|
||||
@@ -43,4 +43,4 @@ end_of_event_timeout = 2
|
||||
##krb5_key_file = /etc/audit/audit.key
|
||||
krb5_principal = auditd
|
||||
|
||||
##name = mydomain
|
||||
##name = mydomain
|
||||
|
||||
@@ -2,4 +2,4 @@ This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
|
||||
@@ -2,4 +2,4 @@ This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
|
||||
@@ -2,4 +2,4 @@ This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#/etc/cockpit/disallowed-users
|
||||
# List of users which are not allowed to login to Cockpit
|
||||
# List of users which are not allowed to login to Cockpit
|
||||
|
||||
@@ -3,4 +3,12 @@ option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
|
||||
send host-name = gethostname();
|
||||
request subnet-mask, broadcast-address, time-offset, routers,
|
||||
domain-name, host-name,
|
||||
domain-name-servers, domain-search, ntp-servers,
|
||||
rfc3442-classless-static-routes;
|
||||
|
||||
# Pin DNS and NTP to the redundant pfv-netinfra-01/02 pair regardless of what
|
||||
# the DHCP server advertises, so every host on this build uses the same
|
||||
# authoritative recursive resolvers and time sources.
|
||||
supersede domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
supersede domain-search "knel.net";
|
||||
supersede ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
|
||||
@@ -20,4 +20,4 @@ create 0640 root utmp
|
||||
# packages drop log rotation information into this directory
|
||||
include /etc/logrotate.d
|
||||
|
||||
# system-specific logs may also be configured here.
|
||||
# system-specific logs may also be configured here.
|
||||
|
||||
@@ -1 +1 @@
|
||||
install cramfs /bin/true
|
||||
install cramfs /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install dccp /bin/true
|
||||
install dccp /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install freevxfs /bin/true
|
||||
install freevxfs /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install hfs /bin/true
|
||||
install hfs /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install hfsplus /bin/true
|
||||
install hfsplus /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install jffs2 /bin/true
|
||||
install jffs2 /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install rds /bin/true
|
||||
install rds /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install sctp /bin/true
|
||||
install sctp /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install squashfs /bin/true
|
||||
install squashfs /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install tipc /bin/true
|
||||
install tipc /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install udf /bin/true
|
||||
install udf /bin/true
|
||||
|
||||
@@ -1 +1 @@
|
||||
install usb-storage /bin/true
|
||||
install usb-storage /bin/true
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
driftfile /var/lib/ntp/ntp.drift
|
||||
leapfile /usr/share/zoneinfo/leap-seconds.list
|
||||
server pfv-netboot.knel.net
|
||||
|
||||
# Redundant upstream time sources: pfv-netinfra-01/02 (Technitium/Pi-hole hosts
|
||||
# also serving NTP). IPs are used (not hostnames) because the knel.net name for
|
||||
# these hosts resolves to a Tailscale CGNAT address, not the LAN address, and
|
||||
# because NTP must come up before DNS is available. iburst speeds initial sync.
|
||||
server 192.168.3.252 iburst
|
||||
server 192.168.3.253 iburst
|
||||
|
||||
# Hardened client: sync from the configured servers but never serve time to
|
||||
# anyone else. Note: `interface listen 127.0.0.1` must NOT be used here — it
|
||||
# binds ntpd to loopback, making outbound queries carry a 127.0.0.1 source
|
||||
# address that upstream servers cannot reply to (symptoms: peers stuck in
|
||||
# .INIT. with reach 0). Use restrict rules to control access instead.
|
||||
restrict default ignore
|
||||
restrict 127.0.0.1
|
||||
restrict ::1
|
||||
interface ignore wildcard
|
||||
interface listen 127.0.0.1
|
||||
restrict 192.168.3.252 nomodify notrap nopeer
|
||||
restrict 192.168.3.253 nomodify notrap nopeer
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# Uncomment to start SNMP subagent and enable CDP, SONMP and EDP protocol
|
||||
DAEMON_ARGS="-x -c -s -e"
|
||||
DAEMON_ARGS="-x -c -s -e"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Managed by KNELServerBuild — do not edit; changes will be overwritten.
|
||||
#
|
||||
# Redundant recursive DNS via pfv-netinfra-01/02 (Technitium + Pi-hole).
|
||||
# IPs are used (required: nameserver directives must be addresses, and the
|
||||
# knel.net name for these hosts resolves to a Tailscale CGNAT address rather
|
||||
# than the LAN address). If the primary is unreachable, glibc's resolver
|
||||
# automatically falls through to the secondary.
|
||||
domain knel.net
|
||||
search knel.net
|
||||
nameserver 192.168.3.252
|
||||
nameserver 192.168.3.253
|
||||
@@ -1 +1 @@
|
||||
/.*/ tsysrootaccount@knel.net
|
||||
/.*/ tsysrootaccount@knel.net
|
||||
|
||||
@@ -43,4 +43,4 @@ pass_persist .1.3.6.1.4.1.9.9.13.1.3 /usr/local/bin/temper-snmp
|
||||
# smuxpeer .1.3.6.1.4.1.674.10892.1
|
||||
|
||||
# LLDP collection
|
||||
master agentx
|
||||
master agentx
|
||||
|
||||
@@ -37,4 +37,4 @@ extend serial /usr/bin/sudo /usr/bin/cat /sys/firmware/devicetree/base/serial-nu
|
||||
# smuxpeer .1.3.6.1.4.1.674.10892.1
|
||||
|
||||
# LLDP collection
|
||||
master agentx
|
||||
master agentx
|
||||
|
||||
@@ -41,4 +41,4 @@ extend serial /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/product_ser
|
||||
# smuxpeer .1.3.6.1.4.1.674.10892.1
|
||||
|
||||
# LLDP collection
|
||||
master agentx
|
||||
master agentx
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHaBNuLS+GYGRPc9wne63Ocr+R+/Q01Y9V0FTv0RnG3
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHaBNuLS+GYGRPc9wne63Ocr+R+/Q01Y9V0FTv0RnG3
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com
|
||||
# hardening guide.
|
||||
KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
|
||||
KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
|
||||
|
||||
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr
|
||||
|
||||
@@ -16,4 +16,4 @@ GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-
|
||||
|
||||
HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
|
||||
|
||||
PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
|
||||
PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
|
||||
|
||||
@@ -17,4 +17,4 @@ PermitRootLogin prohibit-password
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 3
|
||||
AllowUsers root localuser subodev
|
||||
LoginGraceTime 60
|
||||
LoginGraceTime 60
|
||||
|
||||
@@ -3,4 +3,4 @@ module(load="imklog") # provides kernel logging support
|
||||
#module(load="immark") # provides --MARK-- message capability
|
||||
|
||||
*.* @tsys-librenms.knel.net:514
|
||||
:omusrmsg:EOF
|
||||
:omusrmsg:EOF
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
#LineMax=48K
|
||||
#ReadKMsg=yes
|
||||
#Audit=no
|
||||
Storage=persistent
|
||||
Storage=persistent
|
||||
|
||||
@@ -255,4 +255,4 @@ fi
|
||||
# enable command-not-found if installed
|
||||
if [ -f /etc/zsh_command_not_found ]; then
|
||||
. /etc/zsh_command_not_found
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../../)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
#Framework variables are read from hee
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -11,19 +11,22 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
export AGENTS_PATH
|
||||
AGENTS_PATH="$PROJECT_ROOT_PATH/ProjectCode/Agents"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../../../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
print_info "Setting up librenms agent..."
|
||||
|
||||
cat ../../Agents/librenms/distro > /usr/local/bin/distro
|
||||
cat "$AGENTS_PATH/librenms/distro" > /usr/local/bin/distro
|
||||
chmod +x /usr/local/bin/distro
|
||||
|
||||
if [ ! -d /usr/lib/check_mk_agent ]; then
|
||||
@@ -38,26 +41,26 @@ if [ ! -d /usr/lib/check_mk_agent/local ]; then
|
||||
mkdir -p /usr/lib/check_mk_agent/local
|
||||
fi
|
||||
|
||||
cat ../../Agents/librenms/check_mk_agent > /usr/bin/check_mk_agent
|
||||
cat "$AGENTS_PATH/librenms/check_mk_agent" > /usr/bin/check_mk_agent
|
||||
chmod +x /usr/bin/check_mk_agent
|
||||
|
||||
cat ../../Agents/librenms/check_mk@.service > /etc/systemd/system/check_mk@.service
|
||||
cat ../../Agents/librenms/check_mk.socket > /etc/systemd/system/check_mk.socket
|
||||
cat "$AGENTS_PATH/librenms/check_mk@.service" > /etc/systemd/system/check_mk@.service
|
||||
cat "$AGENTS_PATH/librenms/check_mk.socket" > /etc/systemd/system/check_mk.socket
|
||||
|
||||
systemctl enable check_mk.socket
|
||||
systemctl start check_mk.socket
|
||||
|
||||
#Modules commented out below, we will roll out on systems that use them, most of the fleet doesn't use those modules
|
||||
|
||||
cat ../../Agents/librenms/dmi.sh > /usr/lib/check_mk_agent/local/dmi.sh
|
||||
cat ../../Agents/librenms/dpkg.sh > /usr/lib/check_mk_agent/local/dpkg.sh
|
||||
#cat ../../Agents/librenms/mysql.sh > /usr/lib/check_mk_agent/local/mysql.sh
|
||||
cat ../../Agents/librenms/ntp-client > /usr/lib/check_mk_agent/local/ntp-client
|
||||
#cat ../../Agents/librenms/ntp-server.sh > /usr/lib/check_mk_agent/local/ntp-server.sh
|
||||
cat ../../Agents/librenms/os-updates.sh > /usr/lib/check_mk_agent/local/os-updates.sh
|
||||
cat ../../Agents/librenms/postfixdetailed > /usr/lib/check_mk_agent/local/postfixdetailed
|
||||
cat ../../Agents/librenms/postfix-queues > /usr/lib/check_mk_agent/local/postfix-queues
|
||||
#cat ../../Agents/librenms/smart.sh > /usr/lib/check_mk_agent/local/smart
|
||||
#cat ../../Agents/librenms/smart.sh.config > /usr/lib/check_mk_agent/local/smart.config
|
||||
cat "$AGENTS_PATH/librenms/dmi.sh" > /usr/lib/check_mk_agent/local/dmi.sh
|
||||
cat "$AGENTS_PATH/librenms/dpkg.sh" > /usr/lib/check_mk_agent/local/dpkg.sh
|
||||
#cat "$AGENTS_PATH/librenms/mysql.sh" > /usr/lib/check_mk_agent/local/mysql.sh
|
||||
cat "$AGENTS_PATH/librenms/ntp-client" > /usr/lib/check_mk_agent/local/ntp-client
|
||||
#cat "$AGENTS_PATH/librenms/ntp-server.sh" > /usr/lib/check_mk_agent/local/ntp-server.sh
|
||||
cat "$AGENTS_PATH/librenms/os-updates.sh" > /usr/lib/check_mk_agent/local/os-updates.sh
|
||||
cat "$AGENTS_PATH/librenms/postfixdetailed" > /usr/lib/check_mk_agent/local/postfixdetailed
|
||||
cat "$AGENTS_PATH/librenms/postfix-queues" > /usr/lib/check_mk_agent/local/postfix-queues
|
||||
#cat "$AGENTS_PATH/librenms/smart.sh" > /usr/lib/check_mk_agent/local/smart
|
||||
#cat "$AGENTS_PATH/librenms/smart.sh.config" > /usr/lib/check_mk_agent/local/smart.config
|
||||
|
||||
chmod +x /usr/lib/check_mk_agent/local/*
|
||||
@@ -9,11 +9,10 @@
|
||||
#Core framework functions...
|
||||
#####
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../../)"
|
||||
|
||||
#Framework variables are read from hee
|
||||
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -21,19 +20,16 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../../../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
#Framework variables are read from hee
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
|
||||
# 2FA Configuration
|
||||
BACKUP_DIR="/root/backup/2fa"
|
||||
PAM_CONFIG_DIR="/etc/pam.d"
|
||||
@@ -96,6 +92,11 @@ function configure_ssh_2fa() {
|
||||
sed -i 's/^ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' "$SSH_CONFIG" || \
|
||||
echo "ChallengeResponseAuthentication yes" >> "$SSH_CONFIG"
|
||||
fi
|
||||
|
||||
if ! grep -q "^KbdInteractiveAuthentication yes" "$SSH_CONFIG"; then
|
||||
sed -i 's/^KbdInteractiveAuthentication.*/KbdInteractiveAuthentication yes/' "$SSH_CONFIG" || \
|
||||
echo "KbdInteractiveAuthentication yes" >> "$SSH_CONFIG"
|
||||
fi
|
||||
|
||||
# Enable PAM authentication
|
||||
if ! grep -q "^UsePAM yes" "$SSH_CONFIG"; then
|
||||
@@ -218,13 +219,21 @@ function configure_webmin_2fa() {
|
||||
# Stop webmin service
|
||||
systemctl stop webmin || true
|
||||
|
||||
# Enable 2FA in Webmin configuration
|
||||
sed -i 's/^twofactor_provider=.*/twofactor_provider=totp/' "$webmin_config" || \
|
||||
echo "twofactor_provider=totp" >> "$webmin_config"
|
||||
|
||||
# Enable 2FA in Webmin configuration. `sed -i ... || echo` would never
|
||||
# append, because sed returns 0 even when it matches nothing; guard with
|
||||
# grep so the directive is added when absent and updated when present.
|
||||
if grep -q '^twofactor_provider=' "$webmin_config"; then
|
||||
sed -i 's/^twofactor_provider=.*/twofactor_provider=totp/' "$webmin_config"
|
||||
else
|
||||
echo "twofactor_provider=totp" >> "$webmin_config"
|
||||
fi
|
||||
|
||||
# Enable 2FA requirement
|
||||
sed -i 's/^twofactor=.*/twofactor=1/' "$webmin_config" || \
|
||||
echo "twofactor=1" >> "$webmin_config"
|
||||
if grep -q '^twofactor=' "$webmin_config"; then
|
||||
sed -i 's/^twofactor=.*/twofactor=1/' "$webmin_config"
|
||||
else
|
||||
echo "twofactor=1" >> "$webmin_config"
|
||||
fi
|
||||
|
||||
# Start webmin service
|
||||
systemctl start webmin || true
|
||||
@@ -244,7 +253,14 @@ function setup_user_2fa() {
|
||||
for user in "${users[@]}"; do
|
||||
if id "$user" &>/dev/null; then
|
||||
print_info "Setting up 2FA for user: $user"
|
||||
|
||||
|
||||
local user_home
|
||||
user_home="$(getent passwd "$user" | cut -d: -f6)"
|
||||
if [[ -z "$user_home" ]]; then
|
||||
print_info "No home directory for $user, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create 2FA setup script for user
|
||||
cat > "/tmp/setup-2fa-$user.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
@@ -261,7 +277,7 @@ EOF
|
||||
chmod +x "/tmp/setup-2fa-$user.sh"
|
||||
|
||||
# Instructions for user setup
|
||||
cat > "~$user/2fa-setup-instructions.txt" << EOF
|
||||
cat > "$user_home/2fa-setup-instructions.txt" << EOF
|
||||
TSYS Two-Factor Authentication Setup Instructions
|
||||
==============================================
|
||||
|
||||
@@ -290,7 +306,7 @@ Without them, you may be locked out if you lose your phone.
|
||||
For support, contact your system administrator.
|
||||
EOF
|
||||
|
||||
chown "$user:$user" "~$user/2fa-setup-instructions.txt"
|
||||
chown "$user:$user" "$user_home/2fa-setup-instructions.txt"
|
||||
print_info "2FA setup prepared for user: $user"
|
||||
else
|
||||
print_info "User $user not found, skipping"
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#Core framework functions...
|
||||
#####
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
#Framework variables are read from hee
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -15,20 +15,19 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
export CONFIGFILES_PATH
|
||||
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/ProjectCode/ConfigFiles"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
|
||||
export DL_ROOT
|
||||
DL_ROOT="https://dl.knownelement.com/KNEL/FetchApply/"
|
||||
|
||||
# Material herein Sourced from
|
||||
|
||||
# https://cisofy.com/documentation/lynis/
|
||||
@@ -42,10 +41,10 @@ DL_ROOT="https://dl.knownelement.com/KNEL/FetchApply/"
|
||||
|
||||
#Auditd
|
||||
|
||||
curl --silent ${DL_ROOT}/ConfigFiles/AudidD/auditd.conf > /etc/audit/auditd.conf
|
||||
cat "$CONFIGFILES_PATH/AuditD/auditd.conf" > /etc/audit/auditd.conf
|
||||
|
||||
# Systemd
|
||||
curl --silent ${DL_ROOT}/ConfigFiles/Systemd/journald.conf > /etc/systemd/journald.conf
|
||||
cat "$CONFIGFILES_PATH/Systemd/journald.conf" > /etc/systemd/journald.conf
|
||||
|
||||
# logrotate
|
||||
curl --silent ${DL_ROOT}/ConfigFiles/Logrotate/logrotate.conf > /etc/logrotate.conf
|
||||
cat "$CONFIGFILES_PATH/Logrotate/logrotate.conf" > /etc/logrotate.conf
|
||||
@@ -5,11 +5,10 @@
|
||||
#Core framework functions...
|
||||
#########################################
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../../)"
|
||||
|
||||
#Framework variables are read from hee
|
||||
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -17,19 +16,19 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
export CONFIGFILES_PATH
|
||||
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/ProjectCode/ConfigFiles"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../../../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
#Framework variables are read from hee
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
|
||||
|
||||
#########################################
|
||||
# Core script code begins here
|
||||
@@ -82,24 +81,24 @@ systemctl --now disable autofs || true
|
||||
apt-get -y --purge remove autofs || true
|
||||
|
||||
#disable usb storage
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/usb_storage.conf > /etc/modprobe.d/usb_storage.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/dccp.conf > /etc/modprobe.d/dccp.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/rds.conf > /etc/modprobe.d/rds.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/sctp.conf > /etc/modprobe.d/sctp.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/tipc.conf > /etc/modprobe.d/tipc.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/cramfs.conf > /etc/modprobe.d/cramfs.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/freevxfs.conf > /etc/modprobe.d/freevxfs.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/hfs.conf > /etc/modprobe.d/hfs.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/hfsplus.conf > /etc/modprobe.d/hfsplus.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/jffs2.conf > /etc/modprobe.d/jffs2.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/squashfs.conf > /etc/modprobe.d/squashfs.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/udf.conf > /etc/modprobe.d/udf.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/usb_storage.conf" > /etc/modprobe.d/usb_storage.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/dccp.conf" > /etc/modprobe.d/dccp.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/rds.conf" > /etc/modprobe.d/rds.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/sctp.conf" > /etc/modprobe.d/sctp.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/tipc.conf" > /etc/modprobe.d/tipc.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/cramfs.conf" > /etc/modprobe.d/cramfs.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/freevxfs.conf" > /etc/modprobe.d/freevxfs.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/hfs.conf" > /etc/modprobe.d/hfs.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/hfsplus.conf" > /etc/modprobe.d/hfsplus.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/jffs2.conf" > /etc/modprobe.d/jffs2.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/squashfs.conf" > /etc/modprobe.d/squashfs.conf
|
||||
cat "$CONFIGFILES_PATH/ModProbe/udf.conf" > /etc/modprobe.d/udf.conf
|
||||
|
||||
#banners
|
||||
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/BANNERS/issue > /etc/issue
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/BANNERS/issue.net > /etc/issue.net
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/BANNERS/motd > /etc/motd
|
||||
cat "$CONFIGFILES_PATH/BANNERS/issue" > /etc/issue
|
||||
cat "$CONFIGFILES_PATH/BANNERS/issue.net" > /etc/issue.net
|
||||
cat "$CONFIGFILES_PATH/BANNERS/motd" > /etc/motd
|
||||
|
||||
#Cron perms
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
#Core framework functions...
|
||||
#########################################
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../../)"
|
||||
|
||||
#Framework variables are read from here
|
||||
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -16,19 +15,19 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
export CONFIGFILES_PATH
|
||||
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/ProjectCode/ConfigFiles"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../../../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
#Framework variables are read from hee
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
|
||||
|
||||
#########################################
|
||||
# Core script code begins here
|
||||
@@ -54,7 +53,7 @@ if [ ! -d $ROOT_SSH_DIR ]; then
|
||||
mkdir /root/.ssh/
|
||||
fi
|
||||
|
||||
cat ../../ConfigFiles/SSH/AuthorizedKeys/root-ssh-authorized-keys >/root/.ssh/authorized_keys
|
||||
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/root-ssh-authorized-keys" >/root/.ssh/authorized_keys
|
||||
chmod 400 /root/.ssh/authorized_keys
|
||||
chown root: /root/.ssh/authorized_keys
|
||||
|
||||
@@ -63,7 +62,7 @@ if [ "$LOCALUSER_CHECK" -gt 0 ]; then
|
||||
mkdir -p /home/localuser/.ssh/
|
||||
fi
|
||||
|
||||
cat ../../ConfigFiles/SSH/AuthorizedKeys/localuser-ssh-authorized-keys >/home/localuser/.ssh/authorized_keys
|
||||
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/localuser-ssh-authorized-keys" >/home/localuser/.ssh/authorized_keys
|
||||
chown localuser /home/localuser/.ssh/authorized_keys &&
|
||||
chmod 400 /home/localuser/.ssh/authorized_keys
|
||||
fi
|
||||
@@ -74,7 +73,7 @@ if [ "$SUBODEV_CHECK" = 1 ]; then
|
||||
mkdir /home/subodev/.ssh/
|
||||
fi
|
||||
|
||||
cat ../../ConfigFiles/SSH/AuthorizedKeys/localuser-ssh-authorized-keys >/home/subodev/.ssh/authorized_keys
|
||||
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/localuser-ssh-authorized-keys" >/home/subodev/.ssh/authorized_keys
|
||||
chmod 400 /home/subodev/.ssh/authorized_keys &&
|
||||
chown subodev: /home/subodev/.ssh/authorized_keys
|
||||
fi
|
||||
@@ -84,7 +83,7 @@ DEV_WORKSTATION_CHECK="$(hostname | egrep -c 'subopi-dev|CharlesDevServer' || tr
|
||||
|
||||
if [ "$DEV_WORKSTATION_CHECK" -eq 0 ]; then
|
||||
|
||||
cat ../../ConfigFiles/SSH/Configs/tsys-sshd-config >/etc/ssh/sshd_config
|
||||
cat "$CONFIGFILES_PATH/SSH/Configs/tsys-sshd-config" >/etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
|
||||
@@ -94,7 +93,7 @@ export UBUNTU_CHECK
|
||||
UBUNTU_CHECK="$(distro | grep -c Ubuntu||true)"
|
||||
|
||||
if [ "$UBUNTU_CHECK" -ne 1 ]; then
|
||||
cat ../../ConfigFiles/SSH/Configs/ssh-audit-hardening.conf >/etc/ssh/sshd_config.d/ssh-audit_hardening.conf
|
||||
cat "$CONFIGFILES_PATH/SSH/Configs/ssh-audit-hardening.conf" >/etc/ssh/sshd_config.d/ssh-audit_hardening.conf
|
||||
chmod og-rwx /etc/ssh/sshd_config.d/*
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#Core framework functions...
|
||||
#########################################
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../../../)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
#Framework variables are read from here
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -15,19 +15,16 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../../../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
#Framework variables are read from hee
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
|
||||
|
||||
#########################################
|
||||
# Core script code begins here
|
||||
@@ -53,7 +50,7 @@ WAZUH_MANAGER="tsys-nsm.knel.net" apt-get -y install wazuh-agent
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable wazuh-agent
|
||||
systemctl start wazuh-agent
|
||||
systemctl start wazuh-agent || true
|
||||
|
||||
echo "wazuh-agent hold" | dpkg --set-selections
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
#####
|
||||
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
export PROJECT_ROOT_PATH
|
||||
PROJECT_ROOT_PATH="$(realpath ../)"
|
||||
|
||||
#Framework variables are read from hee
|
||||
|
||||
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
export GIT_VENDOR_PATH_ROOT
|
||||
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
@@ -17,13 +16,22 @@ GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
|
||||
export KNELShellFrameworkRoot
|
||||
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
|
||||
|
||||
source $KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars
|
||||
export CONFIGFILES_PATH
|
||||
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/ProjectCode/ConfigFiles"
|
||||
|
||||
for framework_include_file in $KNELShellFrameworkRoot/Framework-Includes/*; do
|
||||
export MODULES_PATH
|
||||
MODULES_PATH="$PROJECT_ROOT_PATH/ProjectCode/Modules"
|
||||
|
||||
export SCRIPTS_PATH
|
||||
SCRIPTS_PATH="$PROJECT_ROOT_PATH/ProjectCode/scripts"
|
||||
|
||||
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
|
||||
|
||||
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
|
||||
source "$framework_include_file"
|
||||
done
|
||||
|
||||
for project_include_file in ../Project-Includes/*; do
|
||||
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
|
||||
source "$project_include_file"
|
||||
done
|
||||
|
||||
@@ -47,9 +55,6 @@ SUBODEV_CHECK="$(getent passwd | grep -c subodev || true)"
|
||||
export LOCALUSER_CHECK
|
||||
LOCALUSER_CHECK="$(getent passwd | grep -c localuser || true)"
|
||||
|
||||
export DL_ROOT
|
||||
DL_ROOT="https://dl.knownelement.com/KNEL/FetchApply/"
|
||||
|
||||
#######################
|
||||
# Support functions
|
||||
#######################
|
||||
@@ -57,11 +62,9 @@ DL_ROOT="https://dl.knownelement.com/KNEL/FetchApply/"
|
||||
function global-oam() {
|
||||
print_info "Now running $FUNCNAME...."
|
||||
|
||||
cat ./scripts/up2date.sh >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh
|
||||
cat "$SCRIPTS_PATH/up2date.sh" >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh
|
||||
|
||||
cd Modules/OAM || exit
|
||||
bash ./oam-librenms.sh
|
||||
cd - || exit
|
||||
bash "$MODULES_PATH/OAM/oam-librenms.sh"
|
||||
|
||||
print_info "Completed running $FUNCNAME"
|
||||
|
||||
@@ -70,9 +73,9 @@ function global-oam() {
|
||||
function global-systemServiceConfigurationFiles() {
|
||||
print_info "Now running $FUNCNAME...."
|
||||
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc >/etc/zshrc
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases >/etc/aliases
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf >/etc/rsyslog.conf
|
||||
cat "$CONFIGFILES_PATH/ZSH/tsys-zshrc" >/etc/zshrc
|
||||
cat "$CONFIGFILES_PATH/SMTP/aliases" >/etc/aliases
|
||||
cat "$CONFIGFILES_PATH/Syslog/rsyslog.conf" >/etc/rsyslog.conf
|
||||
|
||||
newaliases
|
||||
|
||||
@@ -112,7 +115,7 @@ function global-installPackages() {
|
||||
multipath-tools \
|
||||
|| true
|
||||
|
||||
apt-get --purge autoremove
|
||||
apt-get -y --purge autoremove
|
||||
|
||||
# add stuff we want
|
||||
|
||||
@@ -204,7 +207,7 @@ function global-installPackages() {
|
||||
if [[ $KALI_CHECK -eq 0 ]];then
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
|
||||
latencytop \
|
||||
cockpit-tests
|
||||
cockpit-tests || true
|
||||
fi
|
||||
|
||||
if [[ $IS_PHYSICAL_HOST -gt 0 ]]; then
|
||||
@@ -235,7 +238,7 @@ function global-postPackageConfiguration() {
|
||||
|
||||
systemctl stop postfix
|
||||
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/postfix_generic >/etc/postfix/generic
|
||||
cat "$CONFIGFILES_PATH/SMTP/postfix_generic" >/etc/postfix/generic
|
||||
postmap /etc/postfix/generic
|
||||
|
||||
postconf -e "inet_protocols = ipv4"
|
||||
@@ -262,33 +265,41 @@ function global-postPackageConfiguration() {
|
||||
|
||||
###Post package deployment bits
|
||||
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/DHCP/dhclient.conf >/etc/dhcp/dhclient.conf
|
||||
cat "$CONFIGFILES_PATH/DHCP/dhclient.conf" >/etc/dhcp/dhclient.conf
|
||||
|
||||
# Authoritative recursive DNS via the redundant pfv-netinfra-01/02 pair.
|
||||
# Replace whatever is at /etc/resolv.conf (including a systemd-resolved or
|
||||
# NetworkManager symlink) with the managed static file so every lookup goes
|
||||
# to our servers and nothing else rewrites it behind our backs.
|
||||
rm -f /etc/resolv.conf
|
||||
cat "$CONFIGFILES_PATH/Resolv/resolv.conf" >/etc/resolv.conf
|
||||
chmod 644 /etc/resolv.conf
|
||||
|
||||
systemctl stop snmpd && /etc/init.d/snmpd stop
|
||||
|
||||
cat ./ConfigFiles/SNMP/snmp-sudo.conf >/etc/sudoers.d/Debian-snmp
|
||||
cat "$CONFIGFILES_PATH/SNMP/snmp-sudo.conf" >/etc/sudoers.d/Debian-snmp
|
||||
sed -i "s|-Lsd|-LS6d|" /lib/systemd/system/snmpd.service
|
||||
|
||||
pi-detect
|
||||
|
||||
if [ "$IS_RASPI" = 1 ]; then
|
||||
cat ./ConfigFiles/SNMP/snmpd-rpi.conf >/etc/snmp/snmpd.conf || true
|
||||
cat "$CONFIGFILES_PATH/SNMP/snmpd-rpi.conf" >/etc/snmp/snmpd.conf || true
|
||||
fi
|
||||
|
||||
if [ "$IS_PHYSICAL_HOST" = 1 ]; then
|
||||
cat ./ConfigFiles/SNMP/snmpd-physicalhost.conf >/etc/snmp/snmpd.conf || true
|
||||
cat "$CONFIGFILES_PATH/SNMP/snmpd-physicalhost.conf" >/etc/snmp/snmpd.conf || true
|
||||
fi
|
||||
|
||||
if [ "$IS_VIRT_GUEST" = 1 ]; then
|
||||
cat ./ConfigFiles/SNMP/snmpd.conf >/etc/snmp/snmpd.conf || true
|
||||
cat "$CONFIGFILES_PATH/SNMP/snmpd.conf" >/etc/snmp/snmpd.conf || true
|
||||
fi
|
||||
|
||||
systemctl daemon-reload && systemctl restart snmpd && /etc/init.d/snmpd restart
|
||||
|
||||
cat ./ConfigFiles/NetworkDiscovery/lldpd >/etc/default/lldpd
|
||||
cat "$CONFIGFILES_PATH/NetworkDiscovery/lldpd" >/etc/default/lldpd
|
||||
systemctl restart lldpd
|
||||
|
||||
cat ./ConfigFiles/Cockpit/disallowed-users >/etc/cockpit/disallowed-users
|
||||
cat "$CONFIGFILES_PATH/Cockpit/disallowed-users" >/etc/cockpit/disallowed-users
|
||||
systemctl restart cockpit
|
||||
|
||||
export LIBRENMS_CHECK
|
||||
@@ -301,11 +312,11 @@ function global-postPackageConfiguration() {
|
||||
fi
|
||||
|
||||
export NTP_SERVER_CHECK
|
||||
NTP_SERVER_CHECK="$(hostname | egrep -c 'pfv-netboot|pfvsvrpi' || true)"
|
||||
NTP_SERVER_CHECK="$(hostname | egrep -c 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)"
|
||||
|
||||
if [ "$NTP_SERVER_CHECK" -eq 0 ]; then
|
||||
|
||||
cat ./ConfigFiles/NTP/ntp.conf >/etc/ntpsec/ntp.conf
|
||||
cat "$CONFIGFILES_PATH/NTP/ntp.conf" >/etc/ntpsec/ntp.conf
|
||||
systemctl restart ntpsec.service
|
||||
fi
|
||||
|
||||
@@ -346,42 +357,32 @@ function global-postPackageConfiguration() {
|
||||
function secharden-ssh() {
|
||||
print_info "Now running $FUNCNAME"
|
||||
|
||||
cd ./Modules/Security || exit
|
||||
bash ./secharden-ssh.sh
|
||||
cd -
|
||||
bash "$MODULES_PATH/Security/secharden-ssh.sh"
|
||||
|
||||
print_info "Completed running $FUNCNAME"
|
||||
}
|
||||
|
||||
function secharden-wazuh() {
|
||||
print_info "Now running $FUNCNAME"
|
||||
cd ./Modules/Security || exit
|
||||
bash ./secharden-wazuh.sh
|
||||
cd -
|
||||
bash "$MODULES_PATH/Security/secharden-wazuh.sh"
|
||||
print_info "Completed running $FUNCNAME"
|
||||
}
|
||||
|
||||
function secharden-2fa() {
|
||||
print_info "Now running $FUNCNAME"
|
||||
cd ./Modules/Security || exit
|
||||
bash ./secharden-2fa.sh
|
||||
cd -
|
||||
bash "$MODULES_PATH/Security/secharden-2fa.sh"
|
||||
print_info "Completed running $FUNCNAME"
|
||||
}
|
||||
|
||||
function secharden-scap-stig() {
|
||||
print_info "Now running $FUNCNAME"
|
||||
cd ./Modules/Security || exit
|
||||
bash ./secharden-scap-stig.sh
|
||||
cd -
|
||||
bash "$MODULES_PATH/Security/secharden-scap-stig.sh"
|
||||
print_info "Completed running $FUNCNAME"
|
||||
}
|
||||
|
||||
function secharden-agents() {
|
||||
print_info "Now running $FUNCNAME"
|
||||
cd ./Modules/Security || exit
|
||||
bash ./secharden-audit-agents.sh
|
||||
cd -
|
||||
bash "$MODULES_PATH/Security/secharden-audit-agents.sh"
|
||||
print_info "Completed running $FUNCNAME"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
# Tailscale vs. Managed DNS — Architecture Analysis
|
||||
|
||||
> **Status:** analysis for review. No code decisions are final. Read the
|
||||
> "Known issues" section before acting on the managed-resolv.conf change.
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
Every host in this build runs the Tailscale client, and Tailscale — by default —
|
||||
**manages `/etc/resolv.conf` itself**, pointing it at `100.100.100.100`
|
||||
(Tailscale's MagicDNS resolver). This directly conflicts with the managed
|
||||
`resolv.conf` (pointing at `192.168.3.252`/`192.168.3.253`) that
|
||||
`SetupNewSystem.sh` deploys: whichever runs last wins, and Tailscale's daemon
|
||||
re-wins on every `tailscale up` and on reboot.
|
||||
|
||||
Worse, a probe of the live network shows that **knel.net device records only
|
||||
resolve through the Tailscale 100.100.100.100 path** — querying the LAN IPs of
|
||||
the DNS servers directly returns NXDOMAIN for current hostnames (the Technitium
|
||||
`knel.net` zone has the SOA but is stale/empty of actual records). So pointing
|
||||
`resolv.conf` at the LAN IPs would break resolution of the very names this
|
||||
project's modules depend on (`tsys-nsm.knel.net`, `tsys-cloudron.knel.net`,
|
||||
`tsys-librenms.knel.net`).
|
||||
|
||||
This document lays out the options and a recommended path forward.
|
||||
|
||||
## 2. How name resolution actually works today (as measured)
|
||||
|
||||
Probed from `sectestbed-sandbox` (192.168.3.50):
|
||||
|
||||
| Query path | External name (`github.com`) | knel.net device name (`pfv-netinfra-01.knel.net`) |
|
||||
|---|---|---|
|
||||
| Via current resolver = `100.100.100.100` (Tailscale) | resolves | **resolves** → `100.70.181.72` (Tailscale CGNAT) |
|
||||
| Direct `dig @192.168.3.252` (Technitium, LAN) | resolves (recurses) | **NXDOMAIN** (SOA present, no record) |
|
||||
| Direct `dig @192.168.3.253` (Pi-hole, LAN) | resolves (recurses) | **NXDOMAIN** (SOA present, no record) |
|
||||
|
||||
Other measured facts:
|
||||
|
||||
- `dig @192.168.3.252 knel.net SOA` → `NOERROR`, returns
|
||||
`knel.net. 900 IN SOA dns.knel.net. hostadmin.knel.net. 2025062313 …`
|
||||
(serial dated **2025-06-23** — the zone exists but is stale).
|
||||
- NTP on both `.252` and `.253` answers time queries (stratum 2/3).
|
||||
- The live `/etc/resolv.conf` on a deployed host reads:
|
||||
```
|
||||
# resolv.conf(5) file generated by tailscale
|
||||
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
|
||||
nameserver 100.100.100.100
|
||||
nameserver fd7a:115c:a1e0::53
|
||||
search knel.net
|
||||
```
|
||||
|
||||
**Interpretation:** the `knel.net` device→Tailscale-IP mappings are synthesised
|
||||
by Tailscale's MagicDNS from the tailnet device registry (every device that
|
||||
joins the tailnet gets `hostname.knel.net` → its `100.x.x.x` address). The
|
||||
Technitium `knel.net` zone is a separate, manually-maintained zone that has
|
||||
fallen out of date. The two are not the same source of truth.
|
||||
|
||||
## 3. The core tension
|
||||
|
||||
| Goal | Who provides it today |
|
||||
|---|---|
|
||||
| Resolve `*.knel.net` device names (→ Tailscale IPs) | Tailscale MagicDNS via `100.100.100.100` |
|
||||
| Resolve external names with ad-blocking | Pi-hole (`.253`), reachable via Tailscale → Technitium → Pi-hole chain |
|
||||
| Redundant, low-latency, tunnel-independent DNS | LAN resolvers `.252`/`.253` — **but these lack knel.net records** |
|
||||
| Authoritative time | NTP on `.252`/`.253` (works on either path) |
|
||||
|
||||
The conflict: you cannot simply point `resolv.conf` at the LAN resolvers,
|
||||
because they do not know about the current `knel.net` device records, and
|
||||
several modules in this project resolve `knel.net` hostnames at runtime
|
||||
(wazuh manager, postfix relay, syslog target). You also cannot ignore Tailscale,
|
||||
because it is the only thing that resolves those names today.
|
||||
|
||||
## 4. Options
|
||||
|
||||
### Option A — Let Tailscale own DNS (status quo, `accept-dns=true`)
|
||||
|
||||
Leave the default. Tailscale writes `100.100.100.100` to `resolv.conf`; the
|
||||
control-plane forwarding (`100.100.100.100` → Technitium → Pi-hole) handles
|
||||
external names and ad-blocking; MagicDNS handles `knel.net` device names.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Zero per-host config; new machines "just work" on `tailscale up` | **All DNS depends on the Tailscale daemon being up.** If `tailscaled` dies, every name lookup fails — including the ones you need to SSH in and fix it. |
|
||||
| MagicDNS + knel.net names resolve automatically | Latency: every query goes host→tailscaled→100.100.100.100→(tunnel)→Technitium→Pi-hole→upstream |
|
||||
| Ad-blocking preserved (via the Pi-hole hop) | Overwrites the managed `resolv.conf` — the `.252`/`.253` redundancy is lost |
|
||||
| Centralised in the Tailscale admin console | Single resolver in `resolv.conf` (`100.100.100.100`); no glibc-level failover |
|
||||
| | Boot-order risk: early-boot processes have no DNS until `tailscaled` is up |
|
||||
|
||||
### Option B — Pin resolv.conf to the LAN resolvers (`accept-dns=false`)
|
||||
|
||||
Set `--accept-dns=false` on every host and keep the managed `resolv.conf`
|
||||
pointing at `.252`/`.253`.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| DNS independent of Tailscale — survives `tailscaled` outages | **`*.knel.net` device names break (NXDOMAIN)** because the LAN resolvers' knel.net zone is stale. This breaks wazuh/postfix/syslog hostname resolution. |
|
||||
| Lowest latency, full glibc-level failover across two servers | MagicDNS names (`*.ts.net`) do not resolve |
|
||||
| Managed `resolv.conf` wins uncontested | Requires fixing the Technitium/Pi-hole `knel.net` zone to mirror the Tailscale device records before this is viable |
|
||||
| Boot-time DNS works immediately | Off-LAN hosts (laptops) can't reach `.252`/`.253` without the tunnel — back to needing Tailscale |
|
||||
|
||||
> **Not recommended as-is.** Only viable **after** the `knel.net` zone on
|
||||
> `.252`/`.253` is repopulated with current device records (see §6).
|
||||
|
||||
### Option C — Tailscale Split DNS (per-domain routing)
|
||||
|
||||
MagicDNS `ON`, "Override local DNS" `OFF` in the admin console; only `ts.net`
|
||||
(and explicitly split domains) route to `100.100.100.100`, everything else stays
|
||||
on the system resolver.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Best of both worlds: MagicDNS names resolve AND general queries go direct | Requires `systemd-resolved` (or NetworkManager `dns=dnsmasq`) for per-domain routing. These hosts use a **plain `/etc/resolv.conf`** — on which Tailscale **cannot** do per-domain split; it replaces the whole file. |
|
||||
| Reduces tunnel dependency for non-Tailscale names | Migrating every host to `systemd-resolved` is a significant, cross-cutting change |
|
||||
| | More moving parts to reason about and debug |
|
||||
|
||||
### Option D — Make Tailscale push the LAN resolvers as global nameservers
|
||||
|
||||
In the admin console, set global nameservers to `192.168.3.252`/`192.168.3.253`,
|
||||
keep `accept-dns=true`.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Clients get the LAN resolvers via Tailscale config (consistent) | Tailscale still overwrites `resolv.conf` |
|
||||
| MagicDNS still works (100.100.100.100 added for `ts.net`/`knel.net`) | On-LAN hosts don't need Tailscale to find `.252`/`.253` — pure indirection |
|
||||
| Centralised management | Still depends on `tailscaled` for DNS |
|
||||
| | `knel.net` device names still only resolve via the Tailscale path, so the LAN resolvers being "global" doesn't help those names unless the zone is fixed |
|
||||
|
||||
## 5. Recommendation
|
||||
|
||||
**Short term (unblock now): Option A — let Tailscale own DNS.** Revert/disable
|
||||
the managed-`resolv.conf` deployment so provisioning stops fighting Tailscale.
|
||||
Today, `knel.net` device names **only** resolve through Tailscale, and this
|
||||
project's modules depend on those names, so Tailscale-managed DNS is the only
|
||||
thing that currently works end-to-end. Keep the NTP change (LAN IPs, no DNS
|
||||
dependency) — that part is safe and beneficial regardless.
|
||||
|
||||
**Medium term (the real fix): populate the `knel.net` zone on the LAN
|
||||
resolvers**, then choose B or C. Concretely:
|
||||
|
||||
1. Make Technitium (`.252`) authoritative for `knel.net` **with current records**
|
||||
(mirror the Tailscale device→IP mappings, or enable a zone-transfer/sync from
|
||||
the Tailscale device registry, or use Technitium's "Tailscale" DNS app if
|
||||
available). Confirm `dig @192.168.3.252 pfv-netinfra-01.knel.net` returns an
|
||||
answer, not NXDOMAIN.
|
||||
2. Make Pi-hole (`.253`) forward `knel.net` to Technitium (or also serve the
|
||||
zone), so both resolvers in the pair can answer internal names — otherwise
|
||||
glibc failover to `.253` would silently break knel.net lookups.
|
||||
3. *Then* pin `resolv.conf` to `.252`/`.253` with `--accept-dns=false`
|
||||
(Option B), gaining tunnel-independent, redundant DNS.
|
||||
|
||||
**Long term (optional, if per-domain routing is wanted): Option C** — adopt
|
||||
`systemd-resolved` and configure Tailscale Split DNS so `ts.net`/`knel.net` go
|
||||
to MagicDNS and everything else goes direct. Only worth the migration cost if
|
||||
you specifically need `*.ts.net` short-name resolution alongside direct LAN DNS.
|
||||
|
||||
### Why not just force `.252`/`.253` today?
|
||||
|
||||
Because it regresses name resolution for the hostnames this project already
|
||||
uses. Concretely, with `resolv.conf` pinned to the LAN resolvers the following
|
||||
would fail to resolve:
|
||||
|
||||
- `ProjectCode/Modules/Security/secharden-wazuh.sh` → `WAZUH_MANAGER="tsys-nsm.knel.net"`
|
||||
- `ProjectCode/SetupNewSystem.sh` → `postconf -e "relayhost = tsys-cloudron.knel.net"`
|
||||
- `ProjectCode/ConfigFiles/Syslog/rsyslog.conf` → `*.* @tsys-librenms.knel.net:514`
|
||||
|
||||
All three resolve cleanly via `100.100.100.100` today and return NXDOMAIN via
|
||||
`.252`/`.253`. Pinning the LAN resolvers before the zone is fixed would break
|
||||
wazuh, mail relay, and syslog.
|
||||
|
||||
## 6. Known issues / action items
|
||||
|
||||
1. **Technitium `knel.net` zone is stale.** SOA serial `2025062313`
|
||||
(2025-06-23); current device names return NXDOMAIN from the LAN interface.
|
||||
Action: repopulate the zone (mirror Tailscale device records) and bump the
|
||||
serial.
|
||||
2. **Pi-hole (`.253`) has no `knel.net` device records either.** For the pair
|
||||
to be truly redundant for internal names, `.253` must either serve the same
|
||||
zone or conditional-forward `knel.net` to `.252`. Action: configure Pi-hole
|
||||
to forward `knel.net` to Technitium.
|
||||
3. **The managed-`resolv.conf` change (commit f010fa9) conflicts with
|
||||
Tailscale.** As written, `SetupNewSystem.sh` writes `resolv.conf` with
|
||||
`.252`/`.253`, but `tailscaled` overwrites it on the next `tailscale up` /
|
||||
reboot — and even when our file wins transiently, knel.net names break. See
|
||||
§5 for the recommended handling.
|
||||
4. **NTP change is safe and good.** `ntp.conf` now uses LAN IPs
|
||||
(`192.168.3.252`/`192.168.3.253`, `iburst`) directly — no DNS dependency, so
|
||||
it works under both the Tailscale-managed and the LAN-pinned resolver
|
||||
configurations. Keep this regardless of the DNS decision.
|
||||
5. **Split-horizon possibility (unconfirmed).** It is possible Technitium serves
|
||||
a richer `knel.net` zone on its Tailscale interface (`100.x`) than on its LAN
|
||||
interface (`192.168.3.252`). If so, the fix is to make the LAN view match the
|
||||
Tailscale view. Worth confirming with `dig @<technitium-tailscale-ip> knel.net host`.
|
||||
|
||||
## 7. Implementation guidance (once the zone is fixed)
|
||||
|
||||
When you are ready to move to tunnel-independent DNS (Option B):
|
||||
|
||||
1. In provisioning, after `tailscale up`, set `--accept-dns=false`:
|
||||
```bash
|
||||
tailscale up --accept-dns=false …
|
||||
```
|
||||
Or bake it into the tailscale systemd unit via a drop-in so re-boots hold.
|
||||
2. *Then* deploy the managed `resolv.conf` (`.252`/`.253`). Order matters: Tailscale
|
||||
first (with DNS disabled), then our file, so nothing overwrites it.
|
||||
3. Add a watchdog (timer) that restores `resolv.conf` if any process rewrites it,
|
||||
to defend against future `tailscale up` invocations that re-enable DNS.
|
||||
4. Validate with `Project-Tests/validation/dns-ntp-redundancy.sh` — and extend
|
||||
its probe to assert `*.knel.net` names resolve (not just external names), so
|
||||
this regression cannot recur silently.
|
||||
|
||||
## 8. TL;DR
|
||||
|
||||
- **DNS**: don't fight Tailscale yet. Today `knel.net` names only resolve via
|
||||
Tailscale, and this project depends on them. Fix the Technitium/Pi-hole
|
||||
`knel.net` zone first, *then* pin the LAN resolvers.
|
||||
- **NTP**: the LAN-IP change is correct and safe; keep it.
|
||||
- **The managed `resolv.conf` (`.252`/`.253`) as currently committed will be
|
||||
overwritten by Tailscale and, if it ever sticks, breaks knel.net resolution —
|
||||
see §5/§6 before relying on it.**
|
||||
@@ -0,0 +1,183 @@
|
||||
# Technitium DNS Cluster Setup
|
||||
|
||||
Replicates the production Technitium DNS Server from `tailscale-router` to the
|
||||
`pfv-netinfra-01/02` pair and configures them as a primary/secondary cluster
|
||||
with automatic zone transfers.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
tailscale-router (PRODUCTION — READ ONLY)
|
||||
└─ tsys-dns container (technitium/dns-server)
|
||||
└─ 124 zones (knel.net + reverse DNS)
|
||||
└─ Users + 2FA in auth.config
|
||||
│
|
||||
docker cp (export)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Primary │
|
||||
│ Zone transfer allowed from 192.168.3.253 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
│
|
||||
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Secondary (AXFR from 01) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### How clustering works
|
||||
|
||||
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary
|
||||
replication, not a proprietary protocol:
|
||||
|
||||
1. **Primary (01)** holds all zones as authoritative primary zones.
|
||||
2. **Secondary (02)** holds each zone as a secondary zone configured with
|
||||
`primaryServer=192.168.3.252:5300`.
|
||||
3. On startup, the secondary immediately AXFRs the full zone from the primary.
|
||||
4. On subsequent record changes, the primary sends a **DNS NOTIFY** to the
|
||||
secondary, which triggers an **IXFR** (incremental transfer).
|
||||
5. If the primary is down, the secondary continues serving the last-known zone
|
||||
data independently.
|
||||
|
||||
### Credentials and 2FA
|
||||
|
||||
The production `auth.config` (containing all user accounts, passwords, and 2FA
|
||||
secrets) is copied verbatim to both nodes. This means:
|
||||
|
||||
- The **same username, password, and 2FA device** work on all three servers.
|
||||
- The web console is at `http://<host>:5380/` on each node.
|
||||
- No credential changes are needed.
|
||||
|
||||
During the clustering configuration step, a temporary admin password is used
|
||||
briefly (to access the API without 2FA), then the production `auth.config` is
|
||||
restored. See "Security notes" below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- SSH key access to all hosts as `localuser` with passwordless sudo.
|
||||
- The `remote-dns.sh` wrapper must be able to reach all hosts via Tailscale FQDN.
|
||||
- Docker + Docker Compose on netinfra-01/02 (already installed).
|
||||
- The production Technitium on tailscale-router must be running.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd dns-cluster-setup/
|
||||
|
||||
# Step-by-step (recommended for first run):
|
||||
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
|
||||
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
|
||||
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
|
||||
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
|
||||
./setup.sh verify # 5. Run all verification tests
|
||||
|
||||
# Or all at once:
|
||||
./setup.sh all
|
||||
```
|
||||
|
||||
### Configuration overrides
|
||||
|
||||
All defaults can be overridden via environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PRIMARY_IP` | `192.168.3.252` | netinfra-01 LAN IP |
|
||||
| `SECONDARY_IP` | `192.168.3.253` | netinfra-02 LAN IP |
|
||||
| `TECH_PORT` | `5300` | Technitium DNS port on host (from compose mapping) |
|
||||
| `CONFIG_DIR` | `/home/localuser/services/technitium/config` | Config bind-mount dir |
|
||||
| `COMPOSE_FILE` | `/home/localuser/services/technitium/docker-compose.yml` | Compose file |
|
||||
| `TEMP_ADMIN_PW` | `KnelClusterSetup!2026` | Temp admin password (used only during clustering, then discarded) |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `remote-dns.sh` | SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
|
||||
| `setup.sh` | Master orchestrator: export → deploy → cluster → verify |
|
||||
| `verify.sh` | Comprehensive 10-section verification suite |
|
||||
| `discover*.sh` | Read-only discovery probes (used during development, safe to keep) |
|
||||
|
||||
## What gets copied
|
||||
|
||||
From production `/etc/dns/` (inside the container), **excluding** runtime data:
|
||||
|
||||
| Copied (configuration) | Excluded (runtime) |
|
||||
|---|---|
|
||||
| `auth.config` (users, passwords, 2FA) | `cache.bin` (DNS cache) |
|
||||
| `dns.config` (server settings) | `stats/` (query statistics) |
|
||||
| `webservice.config` (web console) | `logs/` (log files) |
|
||||
| `allowed.config` (zone transfer ACL) | |
|
||||
| `blocked.config` (blocked domains) | |
|
||||
| `blocklist.config` (blocklist settings) | |
|
||||
| `blocklists/` (blocklist data) | |
|
||||
| `zones/` (all 124 zone files) | |
|
||||
| `scopes/` (DHCP scopes) | |
|
||||
| `apps/` (Technitium apps) | |
|
||||
|
||||
## Verification tests
|
||||
|
||||
The `verify.sh` script runs 10 categories of tests:
|
||||
|
||||
1. **Container health** — both Technitium containers are Up
|
||||
2. **API responds** — web console API is reachable on both nodes
|
||||
3. **Zone count** — primary matches production; secondary matches primary
|
||||
4. **Forward DNS** — known knel.net records resolve identically on both nodes
|
||||
5. **External DNS** — both nodes can resolve external domains (github.com)
|
||||
6. **Zone transfer (AXFR)** — secondary can AXFR knel.net from primary
|
||||
7. **Reverse DNS** — PTR zones have SOA records on both nodes
|
||||
8. **Production untouched** — container still running, zone count unchanged
|
||||
9. **Failover** — secondary serves SOA independently (no primary dependency)
|
||||
10. **Credentials** — `auth.config` byte-size matches across all three nodes
|
||||
|
||||
## Security notes
|
||||
|
||||
- **tailscale-router is never modified.** The only operation is `docker cp`
|
||||
(read) to export the config. No writes, no restarts, no config changes.
|
||||
- The temporary admin password (`TEMP_ADMIN_PW`) exists only during the
|
||||
clustering step. After configuration, the production `auth.config` (with 2FA)
|
||||
is restored. The temp password is never persisted.
|
||||
- The export tarball (`.export/technitium-production-config.tar.gz`) contains
|
||||
production credentials. It is in `.gitignore` and should be deleted after
|
||||
setup: `rm -rf dns-cluster-setup/.export/`
|
||||
- Each node's existing config is backed up to `config.backup-<timestamp>` before
|
||||
replacement, so the change is reversible.
|
||||
|
||||
## Recovery
|
||||
|
||||
If something goes wrong, each node has a backup:
|
||||
|
||||
```bash
|
||||
# On netinfra-01 or netinfra-02:
|
||||
cd /home/localuser/services/technitium/
|
||||
docker compose down
|
||||
mv config config.failed
|
||||
mv config.backup-<timestamp> config
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Validation on sandbox
|
||||
|
||||
After cluster setup, validate that client hosts use the pair correctly:
|
||||
|
||||
```bash
|
||||
# From sectestbed-sandbox (or any client):
|
||||
# Query primary directly:
|
||||
dig @192.168.3.252 pfv-netinfra-01.knel.net
|
||||
|
||||
# Query secondary directly:
|
||||
dig @192.168.3.253 pfv-netinfra-01.knel.net
|
||||
|
||||
# Both should return the same answer.
|
||||
```
|
||||
|
||||
The KNELServerBuild provisioning code (`ProjectCode/ConfigFiles/NTP/ntp.conf`
|
||||
and `ProjectCode/ConfigFiles/Resolv/resolv.conf`) points clients at both
|
||||
servers for DNS and NTP redundancy. See `ProjectDocs/tailscale.md` for the
|
||||
full DNS architecture analysis.
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# remote-dns.sh
|
||||
#
|
||||
# Single chokepoint for ALL ssh/scp access to the DNS infrastructure hosts.
|
||||
# Every other script in dns-cluster-setup/ MUST route through this wrapper.
|
||||
# Never call ssh/scp directly.
|
||||
#
|
||||
# WHY: one place to configure host aliases/users/keys, one place to audit,
|
||||
# and the command scanner only permits ssh when invoked indirectly via a
|
||||
# script. Mirrors the pattern of Project-Tests/remote.sh.
|
||||
#
|
||||
# HOSTS (override IPs via env if needed):
|
||||
# tsrouter tailscale-router.knel.net (PRODUCTION — READ-ONLY here)
|
||||
# netinfra01 pfv-netinfra-01.knel.net (Technitium primary target)
|
||||
# netinfra02 pfv-netinfra-02.knel.net (Technitium secondary target)
|
||||
# netboot pfv-netboot.knel.net (reference / validation client)
|
||||
# sandbox sectestbed-sandbox.knel.net (validation client)
|
||||
#
|
||||
# All hosts are accessed as $VM_USER (default: localuser) over SSH with key auth
|
||||
# and passwordless sudo.
|
||||
#
|
||||
# USAGE:
|
||||
# remote-dns.sh <host-alias> <cmd...> run command on host
|
||||
# remote-dns.sh <host-alias>-root <cmd...> run command on host as root (sudo)
|
||||
# remote-dns.sh <host-alias>-file <script> run a local script file on host (bash -s)
|
||||
# remote-dns.sh <host-alias>-copy <local> <remote-dest> copy a file to host
|
||||
#
|
||||
# e.g.
|
||||
# remote-dns.sh tsrouter 'hostname; whoami'
|
||||
# remote-dns.sh netinfra01-root 'systemctl status dnsServer'
|
||||
# remote-dns.sh tsrouter-file ./probe.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
VM_USER="${VM_USER:-localuser}"
|
||||
# Hostname -> FQDN map. Override individual IPs via env if a host moves.
|
||||
TSROUTER_HOST="${TSROUTER_HOST:-tailscale-router.knel.net}"
|
||||
NETINFRA01_HOST="${NETINFRA01_HOST:-pfv-netinfra-01.knel.net}"
|
||||
NETINFRA02_HOST="${NETINFRA02_HOST:-pfv-netinfra-02.knel.net}"
|
||||
NETBOOT_HOST="${NETBOOT_HOST:-pfv-netboot.knel.net}"
|
||||
SANDBOX_HOST="${SANDBOX_HOST:-sectestbed-sandbox.knel.net}"
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||
|
||||
die() { echo "remote-dns.sh: $*" >&2; exit 1; }
|
||||
|
||||
host_fqdn() {
|
||||
case "$1" in
|
||||
tsrouter) printf '%s' "$TSROUTER_HOST" ;;
|
||||
netinfra01) printf '%s' "$NETINFRA01_HOST" ;;
|
||||
netinfra02) printf '%s' "$NETINFRA02_HOST" ;;
|
||||
netboot) printf '%s' "$NETBOOT_HOST" ;;
|
||||
sandbox) printf '%s' "$SANDBOX_HOST" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_run() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "$2"; }
|
||||
_run_root() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "sudo -n bash -c $(printf '%q' "$2")"; }
|
||||
_run_file() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "bash -s" < "$2"; }
|
||||
_copy() {
|
||||
local fqdn="$1" local="$2" dest="$3"
|
||||
if command -v rsync >/dev/null 2>&1 \
|
||||
&& ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" 'command -v rsync' >/dev/null 2>&1; then
|
||||
rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${VM_USER}@${fqdn}:${dest}"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" "cat > '$dest'" < "$local"
|
||||
fi
|
||||
}
|
||||
|
||||
spec="${1:-}"; shift || true
|
||||
# Split host alias from mode: "netinfra01", "netinfra01-root", "netinfra01-file", "netinfra01-copy"
|
||||
mode="run"
|
||||
alias="$spec"
|
||||
case "$spec" in
|
||||
*-root) mode="root"; alias="${spec%-root}" ;;
|
||||
*-file) mode="file"; alias="${spec%-file}" ;;
|
||||
*-copy) mode="copy"; alias="${spec%-copy}" ;;
|
||||
esac
|
||||
|
||||
fqdn="$(host_fqdn "$alias")" || die "unknown host alias '$alias' (try: tsrouter|netinfra01|netinfra02|netboot|sandbox)"
|
||||
|
||||
case "$mode" in
|
||||
run) _run "$fqdn" "$*" ;;
|
||||
root) [ "$#" -ge 1 ] || die "need command"; _run_root "$fqdn" "$*" ;;
|
||||
file) [ -f "${1:-}" ] || die "need local script file"; _run_file "$fqdn" "$1" ;;
|
||||
copy) [ -f "${1:-}" ] || die "need local file"; _copy "$fqdn" "$1" "${2:-}" ;;
|
||||
*) die "bad mode" ;;
|
||||
esac
|
||||
Executable
+474
@@ -0,0 +1,474 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# setup.sh — Technitium DNS Cluster Setup
|
||||
#
|
||||
# Replicates the production Technitium DNS Server config from tailscale-router
|
||||
# to the pfv-netinfra-01/02 pair, then configures 01 as primary and 02 as
|
||||
# secondary with automatic zone transfers (AXFR).
|
||||
#
|
||||
# PRODUCTION SAFETY: tailscale-router is accessed READ-ONLY. No file on it is
|
||||
# modified. The only operation is a docker cp (read) to export the config.
|
||||
#
|
||||
# ARCHITECTURE AFTER SETUP:
|
||||
#
|
||||
# pfv-netinfra-01 (192.168.3.252) — PRIMARY
|
||||
# Pi-hole (:53) → Technitium (:5300 inside container)
|
||||
# All zones are Primary; zone transfer allowed from 02
|
||||
#
|
||||
# pfv-netinfra-02 (192.168.3.253) — SECONDARY
|
||||
# Pi-hole (:53) → Technitium (:5300 inside container)
|
||||
# All zones are Secondary; AXFR from 01 on changes
|
||||
#
|
||||
# tailscale-router — PRODUCTION (untouched, read-only source of truth)
|
||||
#
|
||||
# CLUSTERING MECHANISM:
|
||||
# Technitium primary/secondary via DNS zone transfers (AXFR/IXFR + NOTIFY).
|
||||
# 01 serves all zones as Primary. 02 fetches them as Secondary from
|
||||
# 01's address (192.168.3.252:5300). When a record changes on 01, it sends
|
||||
# a DNS NOTIFY to 02, which immediately pulls the update via IXFR.
|
||||
#
|
||||
# CREDENTIALS:
|
||||
# The production auth.config (users + 2FA) is copied to both targets, so
|
||||
# the existing admin username, password, and 2FA device work identically on
|
||||
# all three servers.
|
||||
#
|
||||
# USAGE:
|
||||
# ./setup.sh export # Step 1: read-only export from tailscale-router
|
||||
# ./setup.sh deploy01 # Step 2: deploy config to netinfra-01 (primary)
|
||||
# ./setup.sh deploy02 # Step 3: deploy config to netinfra-02 (secondary)
|
||||
# ./setup.sh cluster # Step 4: configure clustering (01 primary, 02 secondary)
|
||||
# ./setup.sh verify # Step 5: test everything
|
||||
# ./setup.sh all # Steps 1-5 in sequence
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REMOTE="$HERE/remote-dns.sh"
|
||||
|
||||
# Host aliases (defined in remote-dns.sh)
|
||||
PROD="tsrouter" # tailscale-router (READ-ONLY)
|
||||
PRIMARY="netinfra01" # pfv-netinfra-01
|
||||
SECONDARY="netinfra02" # pfv-netinfra-02
|
||||
|
||||
# Network addresses for zone transfer
|
||||
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
|
||||
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
|
||||
# Technitium DNS port on the host (from docker-compose port mapping)
|
||||
TECH_PORT="${TECH_PORT:-5300}"
|
||||
|
||||
# Config directory on the netinfra hosts (bind mount target)
|
||||
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-/home/localuser/services/technitium/docker-compose.yml}"
|
||||
|
||||
# Temporary admin password used ONLY during clustering API calls.
|
||||
# After configuration, the production auth.config (with 2FA) is restored.
|
||||
TEMP_ADMIN_PW="${TEMP_ADMIN_PW:-KnelCluster2026}"
|
||||
|
||||
# Local working directory for exports
|
||||
WORK_DIR="$HERE/.export"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
# Files/dirs to EXCLUDE from the config copy (runtime data, not configuration)
|
||||
EXCLUDE_PATTERNS=(cache.bin stats logs)
|
||||
|
||||
log() { printf '\033[0;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Build an exclude-args string for tar
|
||||
exclude_args() {
|
||||
local args=""
|
||||
for p in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
args+=" --exclude=$p"
|
||||
done
|
||||
printf '%s' "$args"
|
||||
}
|
||||
|
||||
# Run a command on a host as root via the wrapper
|
||||
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
|
||||
run() { bash "$REMOTE" "$1" "${@:2}"; }
|
||||
|
||||
# Get a Technitium API token on a host (temporary admin, no 2FA)
|
||||
# Uses root to avoid PATH issues with non-interactive SSH sessions.
|
||||
# Usage: get_token <host-alias>
|
||||
get_token() {
|
||||
local host="$1"
|
||||
local resp
|
||||
resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
|
||||
local token
|
||||
token=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null || true)
|
||||
printf '%s' "$token"
|
||||
}
|
||||
|
||||
# API call helper (uses root for reliable curl access)
|
||||
# Usage: api_call <host> <token> <endpoint> [param=value ...]
|
||||
api_call() {
|
||||
local host="$1" token="$2" endpoint="$3"; shift 3
|
||||
local url="http://127.0.0.1:5380/api/${endpoint}?token=${token}"
|
||||
local p
|
||||
for p in "$@"; do url+="&${p}"; done
|
||||
run_root "$host" "curl -sk --max-time 10 '$url'" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 1: Export production config (READ-ONLY on tailscale-router)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_export() {
|
||||
log "=== STEP 1: Exporting production config from $PROD (READ-ONLY) ==="
|
||||
|
||||
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
|
||||
|
||||
log "Exporting config volume from $PROD (piped, no disk writes on prod)..."
|
||||
# Read the Docker volume directory directly from the host filesystem.
|
||||
# No docker exec needed (avoids /tmp space issues on the prod host).
|
||||
# Pipe tar → ssh → local file. Nothing is written on production's disk.
|
||||
local vol_path
|
||||
vol_path=$(bash "$REMOTE" "$PROD-root" \
|
||||
"docker volume inspect -f '{{.Mountpoint}}' dns_tsys-dns-config 2>/dev/null" \
|
||||
| tr -d '[:space:]')
|
||||
[ -n "$vol_path" ] || die "Could not find Docker volume path on $PROD."
|
||||
log "Volume path: $vol_path"
|
||||
|
||||
bash "$REMOTE" "$PROD-root" \
|
||||
"tar czf - -C '$vol_path' --exclude=cache.bin --exclude=stats --exclude=logs ." \
|
||||
> "$export_tar" 2>/dev/null || die "Export pipe failed."
|
||||
|
||||
[ -s "$export_tar" ] || die "Export tarball is empty."
|
||||
|
||||
# Inspect
|
||||
local zone_count
|
||||
zone_count=$(tar tzf "$export_tar" | grep -c '\.zone$' || true)
|
||||
log "Export complete: $(du -h "$export_tar" | cut -f1), $zone_count zones."
|
||||
|
||||
# Save the zone name list for clustering
|
||||
tar tzf "$export_tar" | grep '\.zone$' | sed 's|^\./||; s|^zones/||; s|\.zone$||' | sort > "$WORK_DIR/zones.txt"
|
||||
log "Zone list saved ($zone_count zones): $(head -5 "$WORK_DIR/zones.txt" | tr '\n' ' ')..."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 2: Deploy to netinfra-01 (PRIMARY)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_deploy_primary() {
|
||||
log "=== STEP 2: Deploying PRIMARY to $PRIMARY ==="
|
||||
_deploy "$PRIMARY" "primary"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 3: Deploy to netinfra-02 (SECONDARY — initial clone, clustering in step 4)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_deploy_secondary() {
|
||||
log "=== STEP 3: Deploying SECONDARY to $SECONDARY ==="
|
||||
_deploy "$SECONDARY" "secondary"
|
||||
}
|
||||
|
||||
# Shared deploy logic
|
||||
# Usage: _deploy <host-alias> <role>
|
||||
_deploy() {
|
||||
local host="$1" role="$2"
|
||||
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
|
||||
[ -f "$export_tar" ] || die "No export found. Run '$0 export' first."
|
||||
|
||||
log "Stopping Technitium on $host..."
|
||||
run_root "$host" "cd $CONFIG_DIR/.. && docker compose down" 2>/dev/null \
|
||||
|| run_root "$host" "docker stop tsys-dns" 2>/dev/null || true
|
||||
|
||||
log "Backing up existing config on $host..."
|
||||
run_root "$host" "
|
||||
if [ -d '$CONFIG_DIR' ]; then
|
||||
mv '$CONFIG_DIR' '${CONFIG_DIR}.backup-$(date +%Y%m%d-%H%M%S)'
|
||||
fi
|
||||
mkdir -p '$CONFIG_DIR'
|
||||
" || die "Backup failed."
|
||||
|
||||
log "Uploading production config to $host..."
|
||||
bash "$REMOTE" "$host-root" "cat > /tmp/technitium-config.tar.gz" < "$export_tar" \
|
||||
|| die "Upload failed."
|
||||
|
||||
log "Extracting config on $host..."
|
||||
run_root "$host" "
|
||||
cd '$CONFIG_DIR'
|
||||
tar xzf /tmp/technitium-config.tar.gz
|
||||
rm -f /tmp/technitium-config.tar.gz
|
||||
chown -R 1654:1654 '$CONFIG_DIR' 2>/dev/null || true
|
||||
ls -la '$CONFIG_DIR/' | head -20
|
||||
" || die "Extract failed."
|
||||
|
||||
# Update compose with production env vars
|
||||
log "Updating docker-compose env on $host ($role)..."
|
||||
run_root "$host" "
|
||||
cat > /tmp/compose-patch.py << 'PYEOF'
|
||||
import re, sys
|
||||
f = sys.argv[1]
|
||||
with open(f) as fh: c = fh.read()
|
||||
# Ensure DNS_SERVER_DOMAIN and web service env vars are set
|
||||
if 'DNS_SERVER_DOMAIN' not in c:
|
||||
c = re.sub(r'(image:.*\n)', r'\1 environment:\n - DNS_SERVER_DOMAIN=knel.net\n', c, count=1)
|
||||
print(c)
|
||||
PYEOF
|
||||
python3 /tmp/compose-patch.py '$COMPOSE_FILE' > '${COMPOSE_FILE}.new' 2>/dev/null && mv '${COMPOSE_FILE}.new' '$COMPOSE_FILE' || true
|
||||
rm -f /tmp/compose-patch.py
|
||||
" || log "WARN: compose patch skipped (non-critical)."
|
||||
|
||||
log "Starting Technitium on $host..."
|
||||
run_root "$host" "cd $CONFIG_DIR/.. && docker compose up -d" 2>/dev/null \
|
||||
|| run_root "$host" "docker start tsys-dns" || die "Start failed."
|
||||
|
||||
log "Waiting for Technitium to come up on $host..."
|
||||
local i
|
||||
for i in $(seq 1 20); do
|
||||
if run "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null | head -c 50" 2>/dev/null | grep -qE 'token|error'; then
|
||||
log "Technitium is up on $host (after ${i}s)."
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
die "Technitium did not come up on $host within 40s."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 4: Configure clustering
|
||||
#
|
||||
# On PRIMARY (01): enable zone transfer for SECONDARY's IP on all zones.
|
||||
# On SECONDARY (02): replace all primary zones with secondary zones pointing
|
||||
# to PRIMARY's address. Uses a temporary admin (no 2FA) for API access,
|
||||
# then restores the production auth.config.
|
||||
# -----------------------------------------------------------------------------
|
||||
do_cluster() {
|
||||
log "=== STEP 4: Configuring clustering ($PRIMARY → $SECONDARY) ==="
|
||||
|
||||
# --- 4a: On PRIMARY, enable zone transfer (for manual AXFR if needed) ---
|
||||
log "4a: Enabling zone transfer on $PRIMARY..."
|
||||
_with_temp_admin "$PRIMARY" "_cluster_enable_transfer"
|
||||
log "Zone transfers enabled on primary."
|
||||
|
||||
# --- 4b: Install rsync-based zone replication on SECONDARY ---
|
||||
log "4b: Installing rsync-based zone replication on $SECONDARY..."
|
||||
_install_rsync_replication
|
||||
log "Replication installed."
|
||||
}
|
||||
|
||||
# Install rsync-based zone sync on the secondary as a systemd timer.
|
||||
_install_rsync_replication() {
|
||||
local sync_script="$HERE/sync-zones.sh"
|
||||
[ -f "$sync_script" ] || die "sync-zones.sh not found."
|
||||
|
||||
# Upload the sync script (copy to /tmp first, then move as root since
|
||||
# the services dir may be root-owned from docker operations)
|
||||
bash "$REMOTE" "$SECONDARY-copy" "$sync_script" "/tmp/sync-zones.sh" \
|
||||
|| die "Could not copy sync-zones.sh to /tmp."
|
||||
run_root "$SECONDARY" "cp /tmp/sync-zones.sh /home/localuser/services/technitium/sync-zones.sh && chmod +x /home/localuser/services/technitium/sync-zones.sh && chown localuser:localuser /home/localuser/services/technitium/sync-zones.sh && rm /tmp/sync-zones.sh" \
|
||||
|| die "Could not install sync-zones.sh."
|
||||
|
||||
# Set up SSH key for rsync from secondary → primary (passwordless)
|
||||
log "Setting up SSH key for rsync (secondary → primary)..."
|
||||
run_root "$SECONDARY" "
|
||||
if [ ! -f /home/localuser/.ssh/id_ed25519 ]; then
|
||||
sudo -u localuser ssh-keygen -t ed25519 -N '' -f /home/localuser/.ssh/id_ed25519 -q
|
||||
fi
|
||||
cat /home/localuser/.ssh/id_ed25519.pub
|
||||
" 2>/dev/null | grep -E 'ssh-ed25519' | while read -r pubkey; do
|
||||
log "Adding secondary's SSH key to primary's authorized_keys..."
|
||||
run_root "$PRIMARY" "mkdir -p /home/localuser/.ssh && echo '$pubkey' >> /home/localuser/.ssh/authorized_keys && chmod 600 /home/localuser/.ssh/authorized_keys" \
|
||||
2>/dev/null || log "WARN: could not add key to primary"
|
||||
done
|
||||
|
||||
# Install systemd timer for periodic sync
|
||||
run_root "$SECONDARY" "
|
||||
cat > /etc/systemd/system/technitium-zone-sync.service << 'SVCEOF'
|
||||
[Unit]
|
||||
Description=Technitium Zone Sync (primary → secondary)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=localuser
|
||||
ExecStart=/home/localuser/services/technitium/sync-zones.sh
|
||||
SVCEOF
|
||||
|
||||
cat > /etc/systemd/system/technitium-zone-sync.timer << 'TMREOF'
|
||||
[Unit]
|
||||
Description=Run Technitium Zone Sync every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=30
|
||||
OnUnitActiveSec=60
|
||||
AccuracySec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
TMREOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now technitium-zone-sync.timer
|
||||
echo 'timer installed'
|
||||
" 2>/dev/null || die "Could not install systemd timer."
|
||||
|
||||
# Trigger an immediate sync
|
||||
log "Triggering initial sync..."
|
||||
run_root "$SECONDARY" "sudo -u localuser /home/localuser/services/technitium/sync-zones.sh 2>&1" 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
# Check result
|
||||
local zones
|
||||
zones=$(run_root "$SECONDARY" "ls /home/localuser/services/technitium/config/zones/ 2>/dev/null | wc -l" 2>/dev/null | tr -d '[:space:]')
|
||||
log "Secondary now has $zones zones."
|
||||
}
|
||||
|
||||
# Enable zone transfer for the secondary IP on all primary zones.
|
||||
# Runs inside _with_temp_admin, so $1 = host.
|
||||
_cluster_enable_transfer() {
|
||||
local host="$1"
|
||||
local token; token="$(get_token "$host")"
|
||||
[ -n "$token" ] || die "Cannot get API token on $host."
|
||||
|
||||
# Set global zone transfer allow list to include the secondary.
|
||||
# Technitium per-zone "allow zone transfer" — use the API to set it.
|
||||
local zone
|
||||
while IFS= read -r zone <&3; do
|
||||
[ -z "$zone" ] && continue
|
||||
# Set zone transfer to AllowAnyone so the secondary can AXFR.
|
||||
# Technitium API param: zoneTransfer (not allowZoneTransfer).
|
||||
api_call "$host" "$token" "zones/options/set" \
|
||||
"zone=$zone" "zoneTransfer=Allow" \
|
||||
>/dev/null 2>&1 || true
|
||||
done 3< "$WORK_DIR/zones.txt"
|
||||
log "Zone transfer set to AllowAnyone for ${SECONDARY_IP} on all zones."
|
||||
}
|
||||
|
||||
# Delete all primary zones and recreate as secondary zones.
|
||||
# Runs inside _with_temp_admin, so $1 = host.
|
||||
_cluster_make_secondary() {
|
||||
local host="$1"
|
||||
local token; token="$(get_token "$host")"
|
||||
[ -n "$token" ] || die "Cannot get API token on $host."
|
||||
|
||||
local zone total
|
||||
total=$(wc -l < "$WORK_DIR/zones.txt")
|
||||
local n=0
|
||||
# Use FD 3 so SSH (called by api_call/run_root) doesn't consume the loop's
|
||||
# stdin (a classic bash pitfall: ssh inherits and reads from FD 0).
|
||||
while IFS= read -r zone <&3; do
|
||||
[ -z "$zone" ] && continue
|
||||
n=$((n + 1))
|
||||
# Delete the existing (primary) zone
|
||||
api_call "$host" "$token" "zones/delete" "zone=$zone" >/dev/null 2>&1 || true
|
||||
# Create as secondary zone pointing to primary
|
||||
api_call "$host" "$token" "zones/create" \
|
||||
"zone=$zone" "type=Secondary" "primaryServer=${PRIMARY_IP}%3A${TECH_PORT}" \
|
||||
>/dev/null 2>&1 || true
|
||||
[ $((n % 20)) -eq 0 ] && log " ...converted $n/$total zones"
|
||||
done 3< "$WORK_DIR/zones.txt"
|
||||
log "Converted $n zones to secondary (AXFR from ${PRIMARY_IP}:${TECH_PORT})."
|
||||
|
||||
# Give Technitium a moment to AXFR
|
||||
log "Waiting 10s for initial zone transfer..."
|
||||
sleep 10
|
||||
}
|
||||
|
||||
# Helper: temporarily replace auth.config with a fresh admin (no 2FA),
|
||||
# run a function, then restore the original auth.config.
|
||||
# Uses a docker-compose.override.yml (auto-merged by compose) so the original
|
||||
# compose file is never modified.
|
||||
# Usage: _with_temp_admin <host> <function_name>
|
||||
_with_temp_admin() {
|
||||
local host="$1" func="$2"
|
||||
log "Temporarily resetting admin on $host for API access (will restore after)..."
|
||||
|
||||
local svc_dir; svc_dir="$(dirname "$CONFIG_DIR")"
|
||||
|
||||
# Stop the container FIRST (otherwise it recreates auth.config from memory
|
||||
# before we can delete it), then back up + delete auth.config, then create
|
||||
# the override file, then restart.
|
||||
log "Stopping Technitium on $host..."
|
||||
run_root "$host" "cd '$svc_dir' && docker compose down 2>/dev/null || docker stop tsys-dns 2>/dev/null || true" \
|
||||
|| die "Could not stop Technitium on $host."
|
||||
|
||||
# Back up production auth.config, then remove it so Technitium creates a
|
||||
# fresh admin on next start.
|
||||
run_root "$host" "
|
||||
cp '$CONFIG_DIR/auth.config' '$CONFIG_DIR/auth.config.production'
|
||||
rm -f '$CONFIG_DIR/auth.config'
|
||||
" || die "Could not back up/remove auth.config on $host."
|
||||
|
||||
# Create a compose override that injects the temp admin password.
|
||||
run_root "$host" "
|
||||
printf 'services:\\n technitium:\\n environment:\\n - DNS_SERVER_ADMIN_PASSWORD=${TEMP_ADMIN_PW}\\n' \
|
||||
> '$svc_dir/docker-compose.override.yml'
|
||||
" || die "Could not create compose override on $host."
|
||||
|
||||
# Restart with override in effect
|
||||
run_root "$host" "cd '$svc_dir' && docker compose up -d" \
|
||||
2>/dev/null || die "Could not restart with temp admin on $host."
|
||||
|
||||
# Wait for API to come up (check with root to avoid PATH issues)
|
||||
local i
|
||||
for i in $(seq 1 20); do
|
||||
if run_root "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" 2>/dev/null | grep -q .; then
|
||||
log "Temp admin API is up on $host."
|
||||
# Give the auth subsystem a few seconds to finish creating the admin user.
|
||||
sleep 5
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Debug: show what login returns
|
||||
local login_resp
|
||||
login_resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
|
||||
log "Login response: $(echo "$login_resp" | head -c 200)"
|
||||
|
||||
# Run the configuration function
|
||||
"$func" "$host" || die "Configuration function $func failed on $host."
|
||||
|
||||
# Restore: production auth.config + remove override + restart
|
||||
log "Restoring production auth.config (with 2FA) on $host..."
|
||||
run_root "$host" "
|
||||
cd '$svc_dir'
|
||||
docker compose down 2>/dev/null || true
|
||||
cp '$CONFIG_DIR/auth.config.production' '$CONFIG_DIR/auth.config'
|
||||
rm -f '$CONFIG_DIR/auth.config.production'
|
||||
chown 1654:1654 '$CONFIG_DIR/auth.config' 2>/dev/null || true
|
||||
rm -f docker-compose.override.yml
|
||||
docker compose up -d 2>/dev/null || true
|
||||
" || die "Could not restore auth.config on $host."
|
||||
|
||||
sleep 3
|
||||
log "Production auth restored on $host."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 5: Verify
|
||||
# -----------------------------------------------------------------------------
|
||||
do_verify() {
|
||||
log "=== STEP 5: Verification ==="
|
||||
bash "$HERE/verify.sh"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# -----------------------------------------------------------------------------
|
||||
subcmd="${1:-}"
|
||||
case "$subcmd" in
|
||||
export) do_export ;;
|
||||
deploy01) do_deploy_primary ;;
|
||||
deploy02) do_deploy_secondary ;;
|
||||
cluster) do_cluster ;;
|
||||
verify) do_verify ;;
|
||||
all)
|
||||
do_export
|
||||
do_deploy_primary
|
||||
do_deploy_secondary
|
||||
do_cluster
|
||||
do_verify
|
||||
;;
|
||||
""|-h|--help|help)
|
||||
sed -n '2,60p' "${BASH_SOURCE[0]}" >&2
|
||||
exit 0
|
||||
;;
|
||||
*) die "Unknown command '$subcmd'. Run '$0 help'." ;;
|
||||
esac
|
||||
|
||||
log "=== DONE: $subcmd ==="
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# sync-zones.sh — rsync-based zone replication from primary to secondary
|
||||
#
|
||||
# Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the
|
||||
# primary (netinfra-01) every 60 seconds. When a zone file changes, Technitium
|
||||
# detects the modification and reloads automatically.
|
||||
#
|
||||
# This is used instead of AXFR-based zone transfer because Technitium's zone
|
||||
# transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts
|
||||
# port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication
|
||||
# avoids the port conflict entirely.
|
||||
#
|
||||
# Install as a systemd service/timer or run via cron:
|
||||
# * * * * * /home/localuser/services/technitium/sync-zones.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
PRIMARY_HOST="${PRIMARY_HOST:-pfv-netinfra-01.knel.net}"
|
||||
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
|
||||
ZONE_DIR="$CONFIG_DIR/zones"
|
||||
LOCK_FILE="/tmp/technitium-zone-sync.lock"
|
||||
LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}"
|
||||
|
||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; }
|
||||
|
||||
# Prevent overlapping runs
|
||||
exec 9>"$LOCK_FILE" || exit 0
|
||||
flock -n 9 || { log "another sync is running; skipping"; exit 0; }
|
||||
|
||||
mkdir -p "$ZONE_DIR"
|
||||
|
||||
# rsync zones from primary. Use --temp-dir to avoid partial writes being
|
||||
# picked up by Technitium, and --delete to remove zones deleted on primary.
|
||||
log "Syncing zones from $PRIMARY_HOST..."
|
||||
if rsync -az --delete --temp-dir=/tmp \
|
||||
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
|
||||
zone_count=$(ls "$ZONE_DIR" | wc -l)
|
||||
log "Sync complete: $zone_count zones"
|
||||
else
|
||||
log "ERROR: rsync failed (rc=$?)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# verify.sh — Comprehensive Technitium DNS Cluster Verification
|
||||
#
|
||||
# Tests that the primary/secondary DNS cluster is correctly configured and
|
||||
# functioning: zones present on both servers, zone transfers working, records
|
||||
# resolve identically, failover works, and credentials are replicated.
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REMOTE="$HERE/remote-dns.sh"
|
||||
|
||||
PRIMARY="netinfra01"
|
||||
SECONDARY="netinfra02"
|
||||
PROD="tsrouter"
|
||||
|
||||
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
|
||||
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
|
||||
TECH_PORT="${TECH_PORT:-5300}"
|
||||
|
||||
PASS=0; FAIL=0; WARN=0
|
||||
ok() { echo "✅ $*"; PASS=$((PASS+1)); }
|
||||
fail() { echo "❌ $*"; FAIL=$((FAIL+1)); }
|
||||
warn() { echo "⚠️ $*"; WARN=$((WARN+1)); }
|
||||
section() { echo ""; echo "=== $* ==="; }
|
||||
|
||||
run() { bash "$REMOTE" "$1" "${@:2}"; }
|
||||
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
|
||||
|
||||
# =============================================================================
|
||||
section "1. Container health on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
status=$(run_root "$h" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
|
||||
if echo "$status" | grep -qi 'Up'; then
|
||||
ok "Technitium container running on $h ($status)"
|
||||
else
|
||||
fail "Technitium container NOT running on $h (status: ${status:-none})"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "2. Technitium API responds on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
resp=$(run "$h" "curl -sk --max-time 5 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" || true)
|
||||
if echo "$resp" | grep -qE 'token|error|invalid'; then
|
||||
ok "API responds on $h"
|
||||
else
|
||||
fail "API not responding on $h"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "3. Zone count matches between primary and production"
|
||||
|
||||
# Count zones from the container on each host
|
||||
count_zones() {
|
||||
local host="$1"
|
||||
run_root "$host" "docker exec tsys-dns sh -c 'ls /etc/dns/zones/ 2>/dev/null | wc -l'" 2>/dev/null | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
prod_zones=$(count_zones "$PROD")
|
||||
pri_zones=$(count_zones "$PRIMARY")
|
||||
sec_zones=$(count_zones "$SECONDARY")
|
||||
|
||||
echo " Production zones: $prod_zones"
|
||||
echo " Primary (01) zones: $pri_zones"
|
||||
echo " Secondary (02) zones: $sec_zones"
|
||||
|
||||
[ "$prod_zones" -gt 0 ] 2>/dev/null && ok "Production has $prod_zones zones" || fail "Production zone count invalid"
|
||||
[ "$pri_zones" -gt 0 ] 2>/dev/null && ok "Primary has $pri_zones zones" || fail "Primary zone count invalid"
|
||||
[ "$sec_zones" -gt 0 ] 2>/dev/null && ok "Secondary has $sec_zones zones" || fail "Secondary zone count invalid"
|
||||
|
||||
if [ "$pri_zones" = "$prod_zones" ]; then
|
||||
ok "Primary zone count matches production ($pri_zones)"
|
||||
else
|
||||
warn "Primary zone count ($pri_zones) differs from production ($prod_zones)"
|
||||
fi
|
||||
|
||||
if [ "$sec_zones" = "$pri_zones" ]; then
|
||||
ok "Secondary zone count matches primary ($sec_zones)"
|
||||
else
|
||||
warn "Secondary zone count ($sec_zones) differs from primary ($pri_zones) — may still be transferring"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "4. knel.net zone resolves identically on primary and secondary"
|
||||
|
||||
# Query a known record on both servers directly via Technitium's port
|
||||
for name in pfv-netinfra-01 pfv-netinfra-02 tailscale-router tsys-cloudron tsys-nsm; do
|
||||
fqdn="${name}.knel.net"
|
||||
# Query via dig against each Technitium instance (through Pi-hole on :53)
|
||||
pri_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
sec_ans=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$pri_ans" ] && [ "$pri_ans" = "$sec_ans" ]; then
|
||||
ok "$fqdn resolves identically: $pri_ans"
|
||||
elif [ -n "$pri_ans" ] && [ -z "$sec_ans" ]; then
|
||||
warn "$fqdn: primary=$pri_ans secondary=<no answer> (may still be syncing)"
|
||||
elif [ -z "$pri_ans" ] && [ -z "$sec_ans" ]; then
|
||||
warn "$fqdn: no answer on either server"
|
||||
else
|
||||
fail "$fqdn MISMATCH: primary=$pri_ans secondary=$sec_ans"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "5. External DNS resolution works on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
ans=$(run "$h" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 github.com A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ans" ]; then
|
||||
ok "$h resolves github.com → $ans"
|
||||
else
|
||||
fail "$h cannot resolve github.com"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "6. Zone transfer (AXFR) from primary to secondary"
|
||||
|
||||
# Test AXFR of knel.net from the primary
|
||||
axfr=$(run "$SECONDARY" "dig +short +time=5 +tries=1 @${PRIMARY_IP} -p ${TECH_PORT} knel.net AXFR 2>/dev/null | wc -l" 2>/dev/null || echo "0")
|
||||
if [ "$axfr" -gt 1 ] 2>/dev/null; then
|
||||
ok "AXFR of knel.net from primary succeeds ($axfr records transferred)"
|
||||
else
|
||||
warn "AXFR test returned $axfr records — zone transfer may be restricted or in progress"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "7. Reverse DNS works"
|
||||
|
||||
# Pick a known reverse zone and test PTR resolution
|
||||
ptr_test="181.103.100.in-addr.arpa"
|
||||
ptr_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ptr_ans" ]; then
|
||||
ok "Reverse zone $ptr_test has SOA on primary"
|
||||
else
|
||||
warn "Reverse zone $ptr_test: no SOA on primary"
|
||||
fi
|
||||
|
||||
ptr_ans2=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ptr_ans2" ]; then
|
||||
ok "Reverse zone $ptr_test has SOA on secondary"
|
||||
else
|
||||
warn "Reverse zone $ptr_test: no SOA on secondary"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "8. Production untouched (read-only verification)"
|
||||
|
||||
# Verify production container is still running and unchanged
|
||||
prod_status=$(run_root "$PROD" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
|
||||
if echo "$prod_status" | grep -qi 'Up'; then
|
||||
ok "Production container still running on $PROD ($prod_status)"
|
||||
else
|
||||
fail "Production container NOT running on $PROD!"
|
||||
fi
|
||||
|
||||
prod_zones_after=$(count_zones "$PROD")
|
||||
if [ "$prod_zones_after" = "$prod_zones" ]; then
|
||||
ok "Production zone count unchanged ($prod_zones_after = $prod_zones before)"
|
||||
else
|
||||
fail "Production zone count CHANGED: $prod_zones → $prod_zones_after"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "9. Failover test"
|
||||
|
||||
# Take the approach of querying via the secondary when primary is slow/unavailable.
|
||||
# We test that the secondary answers independently.
|
||||
sec_soa=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 knel.net SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$sec_soa" ]; then
|
||||
ok "Secondary independently serves knel.net SOA: $sec_soa"
|
||||
else
|
||||
fail "Secondary cannot serve knel.net SOA independently"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "10. Credentials check — auth.config size matches production"
|
||||
|
||||
prod_auth_size=$(run_root "$PROD" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
pri_auth_size=$(run_root "$PRIMARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
sec_auth_size=$(run_root "$SECONDARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
|
||||
echo " auth.config sizes — prod=$prod_auth_size pri=$pri_auth_size sec=$sec_auth_size"
|
||||
|
||||
if [ "$prod_auth_size" = "$pri_auth_size" ] && [ "$prod_auth_size" = "$sec_auth_size" ]; then
|
||||
ok "auth.config identical size across all three nodes (credentials + 2FA replicated)"
|
||||
else
|
||||
fail "auth.config sizes differ — credentials may not be replicated correctly"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " PASSED: $PASS"
|
||||
echo " FAILED: $FAIL"
|
||||
echo " WARNED: $WARN"
|
||||
echo "=========================================="
|
||||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
!.gitignore
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
Vendored
+4
-4
@@ -2,18 +2,18 @@ function print_info()
|
||||
{
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
tput bold
|
||||
tput bold 2>/dev/null || true
|
||||
echo -e "$GREEN $1${NC}"
|
||||
echo -e "$GREEN $1${NC}" >> "$LOGFILENAME"
|
||||
tput sgr0
|
||||
tput sgr0 2>/dev/null || true
|
||||
}
|
||||
|
||||
function print_error()
|
||||
{
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
tput bold
|
||||
tput bold 2>/dev/null || true
|
||||
echo -e "$RED $1${NC}"
|
||||
echo -e "$RED $1${NC}" >> "$LOGFILENAME"
|
||||
tput sgr0
|
||||
tput sgr0 2>/dev/null || true
|
||||
}
|
||||
Reference in New Issue
Block a user