Pulse/docs/PULSE_SENSOR_PROXY_HARDENING.md
rcourtman fcd8b62705 refactor: Rename install-temp-proxy.sh to install-sensor-proxy.sh
Complete the pulse-sensor-proxy rename by updating the installer script name and all references to it.

Updated:
- Renamed scripts/install-temp-proxy.sh → scripts/install-sensor-proxy.sh
- Updated all documentation references
- Updated install.sh references
- Updated build-release.sh comments
2025-10-13 13:23:53 +00:00

962 lines
28 KiB
Markdown

# Pulse Temperature Proxy - Security Hardening Guide
## Overview
The `pulse-sensor-proxy` is a host-side service that provides secure temperature monitoring for containerized Pulse deployments. It addresses a critical security concern: SSH keys stored inside LXC containers can be exfiltrated if the container is compromised.
**Architecture:**
- Host-side proxy runs with minimal privileges on each Proxmox node
- Containerized Pulse communicates via Unix socket (`/run/pulse-sensor-proxy/pulse-sensor-proxy.sock`)
- Proxy authenticates containers using Linux `SO_PEERCRED` (UID/PID verification)
- SSH keys never leave the host filesystem
**Threat Model:**
- ✅ Container compromise cannot access SSH keys
- ✅ Container cannot directly SSH to cluster nodes
- ✅ Rate limiting prevents abuse via socket
- ✅ IP restrictions on SSH keys limit lateral movement
- ✅ Audit logging tracks all temperature requests
## Prerequisites
- Proxmox VE 7.0+ or Proxmox Backup Server 2.0+
- LXC container running Pulse (unprivileged recommended)
- Root access to Proxmox host(s)
- `lm-sensors` installed on all nodes
- Cluster SSH access configured (root passwordless SSH between nodes)
## Host Hardening
### Service Account
The proxy runs as the `pulse-sensor-proxy` user with these characteristics:
- System account (no login shell: `/usr/sbin/nologin`)
- No home directory
- Dedicated group: `pulse-sensor-proxy`
- Owns `/var/lib/pulse-sensor-proxy` and `/run/pulse-sensor-proxy`
**Verify service account:**
```bash
# Check user exists
id pulse-sensor-proxy
# Expected output:
# uid=XXX(pulse-sensor-proxy) gid=XXX(pulse-sensor-proxy) groups=XXX(pulse-sensor-proxy)
# Check shell (should be /usr/sbin/nologin)
getent passwd pulse-sensor-proxy | cut -d: -f7
```
### Systemd Unit Security
The systemd unit includes comprehensive hardening directives:
**Key security features:**
- `User=pulse-sensor-proxy` / `Group=pulse-sensor-proxy` - Unprivileged execution
- `NoNewPrivileges=true` - Prevents privilege escalation
- `ProtectSystem=strict` - Read-only `/usr`, `/boot`, `/efi`
- `ProtectHome=true` - Inaccessible `/home`, `/root`, `/run/user`
- `PrivateTmp=true` - Isolated `/tmp` and `/var/tmp`
- `SystemCallFilter=@system-service` - Restricted syscalls
- `CapabilityBoundingSet=` - No capabilities granted
- `RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6` - Socket restrictions
**Verify systemd security:**
```bash
# Check service status
systemctl status pulse-sensor-proxy
# Verify user/group
ps aux | grep pulse-sensor-proxy | grep -v grep
# Expected: pulse-sensor-proxy user, not root
# Check systemd security settings
systemctl show pulse-sensor-proxy | grep -E '(User=|NoNewPrivileges|ProtectSystem|CapabilityBoundingSet)'
```
### File Permissions
**Critical paths and ownership:**
```
/var/lib/pulse-sensor-proxy/ pulse-sensor-proxy:pulse-sensor-proxy 0750
├── ssh/ pulse-sensor-proxy:pulse-sensor-proxy 0700
│ ├── id_ed25519 pulse-sensor-proxy:pulse-sensor-proxy 0600
│ └── id_ed25519.pub pulse-sensor-proxy:pulse-sensor-proxy 0640
└── ssh.d/ pulse-sensor-proxy:pulse-sensor-proxy 0750
├── next/ pulse-sensor-proxy:pulse-sensor-proxy 0750
└── prev/ pulse-sensor-proxy:pulse-sensor-proxy 0750
/run/pulse-sensor-proxy/ pulse-sensor-proxy:pulse-sensor-proxy 0775
└── pulse-sensor-proxy.sock pulse-sensor-proxy:pulse-sensor-proxy 0777
```
**Verify permissions:**
```bash
# Check base directory
ls -ld /var/lib/pulse-sensor-proxy/
# Expected: drwxr-x--- pulse-sensor-proxy pulse-sensor-proxy
# Check SSH keys
ls -l /var/lib/pulse-sensor-proxy/ssh/
# Expected:
# -rw------- pulse-sensor-proxy pulse-sensor-proxy id_ed25519
# -rw-r----- pulse-sensor-proxy pulse-sensor-proxy id_ed25519.pub
# Check socket directory (note: 0775 for container access)
ls -ld /run/pulse-sensor-proxy/
# Expected: drwxrwxr-x pulse-sensor-proxy pulse-sensor-proxy
```
**Why 0775 on socket directory?**
The socket directory needs `0775` (not `0770`) to allow the container's unprivileged UID (e.g., 1001) to traverse into the directory and access the socket. The socket itself is `0777` as access control is enforced via `SO_PEERCRED`.
## LXC Container Requirements
### Configuration Summary
| Setting | Value | Purpose |
|---------|-------|---------|
| `lxc.idmap` | `u 0 100000 65536`<br>`g 0 100000 65536` | Unprivileged UID/GID mapping |
| `lxc.apparmor.profile` | `generated` or custom | AppArmor confinement |
| `lxc.cap.drop` | `sys_admin` (optional) | Drop dangerous capabilities |
| `lxc.mount.entry` | Directory-level bind mount | Socket access from container |
### Sample LXC Configuration
**In `/etc/pve/lxc/<VMID>.conf`:**
```ini
# Unprivileged container (required)
unprivileged: 1
# AppArmor profile (recommended)
lxc.apparmor.profile: generated
# Drop CAP_SYS_ADMIN if feasible (optional but recommended)
# WARNING: May break some container management operations
lxc.cap.drop: sys_admin
# Bind mount proxy socket directory (REQUIRED)
# Note: Directory-level mount, not socket-level (socket is recreated by systemd)
lxc.mount.entry: /run/pulse-sensor-proxy run/pulse-sensor-proxy none bind,create=dir 0 0
```
**Key points:**
- **Directory-level mount**: Mount `/run/pulse-sensor-proxy` directory, not the socket file itself
- **Why directory mount?** Systemd recreates the socket on restart; socket-level mounts break on recreation
- **Mode 0775**: Socket directory needs group+other execute permissions for container UID traversal
- **Socket 0777**: Actual socket is world-writable; security enforced via `SO_PEERCRED` authentication
### Runtime Verification
**Check container is unprivileged:**
```bash
# On host
pct config <VMID> | grep unprivileged
# Expected: unprivileged: 1
# Inside container
cat /proc/self/uid_map
# Expected: 0 100000 65536 (or similar)
# NOT: 0 0 4294967295 (privileged)
```
**Check AppArmor confinement:**
```bash
# Inside container
cat /proc/self/attr/current
# Expected: lxc-<vmid>_</var/lib/lxc> (enforcing) or similar
# NOT: unconfined
```
**Check namespace isolation:**
```bash
# Inside container
ls -li /proc/self/ns/
# Each namespace should have a unique inode number, different from host
```
**Check capabilities:**
```bash
# Inside container
capsh --print | grep Current
# Should show limited capability set
# If lxc.cap.drop: sys_admin is set, CAP_SYS_ADMIN should be absent
```
**Check bind mount:**
```bash
# Inside container
ls -la /run/pulse-sensor-proxy/
# Expected: pulse-sensor-proxy.sock visible
# Test socket access (requires Pulse to attempt connection)
socat - UNIX-CONNECT:/run/pulse-sensor-proxy/pulse-sensor-proxy.sock
# Should connect (may timeout waiting for input, but connection succeeds)
```
## Key Management
### SSH Key Restrictions
All SSH keys deployed to cluster nodes include these restrictions:
- `command="sensors -j"` - Forced command (only sensors allowed)
- `from="<subnets>"` - IP address restrictions
- `no-port-forwarding` - Disable port forwarding
- `no-X11-forwarding` - Disable X11 forwarding
- `no-agent-forwarding` - Disable agent forwarding
- `no-pty` - Disable PTY allocation
**Example authorized_keys entry:**
```
from="192.168.0.0/24,10.0.0.0/8",command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... pulse-sensor-proxy
```
**Configure allowed subnets:**
Create `/etc/pulse-sensor-proxy/config.yaml`:
```yaml
allowed_source_subnets:
- "192.168.0.0/24" # LAN subnet
- "10.0.0.0/8" # VPN subnet
```
Or use environment variable:
```bash
# In /etc/default/pulse-sensor-proxy (loaded by systemd)
PULSE_SENSOR_PROXY_ALLOWED_SUBNETS="192.168.0.0/24,10.0.0.0/8"
```
**Auto-detection:**
If no subnets are configured, the proxy auto-detects host IP addresses and uses them as `/32` (IPv4) or `/128` (IPv6) CIDRs. This is secure but brittle (breaks if host IP changes). Explicit configuration is recommended.
**Verify SSH restrictions:**
```bash
# On any cluster node
grep pulse-sensor-proxy /root/.ssh/authorized_keys
# Expected format:
# from="...",command="sensors -j",no-* ssh-ed25519 AAAA... pulse-sensor-proxy
```
### Key Rotation
**Rotation cadence:**
- Recommended: Every 90 days
- Minimum: Every 180 days
- After incident: Immediately
**Rotation workflow:**
The `pulse-sensor-proxy-rotate-keys.sh` script performs staged rotation with verification:
1. **Dry-run (recommended first):**
```bash
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --dry-run
```
Shows what would happen without making changes.
2. **Perform rotation:**
```bash
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh
```
**What happens:**
- Generates new Ed25519 keypair in `/var/lib/pulse-sensor-proxy/ssh.d/next/`
- Pushes new key to all cluster nodes (via RPC `ensure_cluster_keys`)
- Verifies SSH connectivity with new key on each node
- Atomically swaps keys:
- Current `/ssh/``/ssh.d/prev/` (backup)
- Staging `/ssh.d/next/``/ssh/` (active)
- Old keys preserved in `/ssh.d/prev/` for rollback
3. **If rotation fails, rollback:**
```bash
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --rollback
```
Restores previous keypair from `/ssh.d/prev/` and re-pushes to cluster nodes.
**Post-rotation verification:**
```bash
# Check new key timestamp
stat /var/lib/pulse-sensor-proxy/ssh/id_ed25519
# Verify all nodes have new key
for node in pve1 pve2 pve3; do
echo "=== $node ==="
ssh root@$node "grep pulse-sensor-proxy /root/.ssh/authorized_keys | tail -1"
done
# Test temperature fetch via proxy
curl -s --unix-socket /run/pulse-sensor-proxy/pulse-sensor-proxy.sock \
-d '{"correlation_id":"test","method":"get_temp","params":{"node":"pve1"}}' \
| jq .
```
### Automated Rotation (Optional)
**Create systemd timer:**
`/etc/systemd/system/pulse-sensor-proxy-key-rotation.service`:
```ini
[Unit]
Description=Rotate pulse-sensor-proxy SSH keys
After=pulse-sensor-proxy.service
Requires=pulse-sensor-proxy.service
[Service]
Type=oneshot
ExecStart=/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh
StandardOutput=journal
StandardError=journal
```
`/etc/systemd/system/pulse-sensor-proxy-key-rotation.timer`:
```ini
[Unit]
Description=Rotate pulse-sensor-proxy SSH keys every 90 days
Requires=pulse-sensor-proxy-key-rotation.service
[Timer]
OnCalendar=quarterly
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
```
**Enable timer:**
```bash
systemctl daemon-reload
systemctl enable --now pulse-sensor-proxy-key-rotation.timer
# Check next run
systemctl list-timers pulse-sensor-proxy-key-rotation.timer
```
## Monitoring & Auditing
### Metrics Endpoint
The proxy exposes Prometheus metrics on `127.0.0.1:9127` by default.
**Available metrics:**
- `pulse_proxy_rpc_requests_total{method, result}` - RPC request counter
- `pulse_proxy_rpc_latency_seconds{method}` - RPC handler latency histogram
- `pulse_proxy_ssh_requests_total{node, result}` - SSH request counter per node
- `pulse_proxy_ssh_latency_seconds{node}` - SSH latency histogram per node
- `pulse_proxy_queue_depth` - Concurrent RPC requests (gauge)
- `pulse_proxy_rate_limit_hits_total` - Rejected requests due to rate limiting
- `pulse_proxy_build_info{version}` - Build metadata
**Configure metrics address:**
In `/etc/default/pulse-sensor-proxy`:
```bash
# Listen on all interfaces (WARNING: exposes metrics externally)
PULSE_SENSOR_PROXY_METRICS_ADDR="0.0.0.0:9127"
# Disable metrics
PULSE_SENSOR_PROXY_METRICS_ADDR="disabled"
```
**Test metrics endpoint:**
```bash
curl -s http://127.0.0.1:9127/metrics | grep pulse_proxy
```
### Prometheus Integration
**Sample scrape configuration:**
```yaml
scrape_configs:
- job_name: 'pulse-sensor-proxy'
static_configs:
- targets:
- 'pve1:9127'
- 'pve2:9127'
- 'pve3:9127'
relabel_configs:
- source_labels: [__address__]
regex: '([^:]+):.+'
target_label: instance
```
### Alert Rules
**Recommended Prometheus alerts:**
```yaml
groups:
- name: pulse-sensor-proxy
rules:
# High SSH failure rate
- alert: PulseProxySSHFailureRate
expr: |
rate(pulse_proxy_ssh_requests_total{result="error"}[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "High SSH failure rate on {{ $labels.instance }}"
description: "{{ $value | humanize }} SSH requests/sec failing"
# Rate limiting active
- alert: PulseProxyRateLimiting
expr: |
rate(pulse_proxy_rate_limit_hits_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Rate limiting active on {{ $labels.instance }}"
description: "Proxy rejecting requests due to rate limits"
# High queue depth
- alert: PulseProxyQueueDepth
expr: pulse_proxy_queue_depth > 5
for: 5m
labels:
severity: warning
annotations:
summary: "High RPC queue depth on {{ $labels.instance }}"
description: "{{ $value }} concurrent requests (threshold: 5)"
# Proxy down
- alert: PulseProxyDown
expr: up{job="pulse-sensor-proxy"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Pulse proxy down on {{ $labels.instance }}"
```
### Audit Logging
**Log format:**
All RPC requests are logged with structured fields:
- `corr_id` - Correlation ID (UUID, tracks request lifecycle)
- `uid` / `pid` - Peer credentials from `SO_PEERCRED`
- `method` - RPC method called (`get_temp`, `register_nodes`, `ensure_cluster_keys`)
**Example log entries:**
```json
{"level":"info","corr_id":"a7f3d..","uid":1001,"pid":12345,"method":"get_temp","node":"pve1","msg":"RPC request"}
{"level":"info","corr_id":"a7f3d..","node":"pve1","latency_ms":245,"msg":"Temperature fetch successful"}
```
**Query logs:**
```bash
# All RPC requests in last hour
journalctl -u pulse-sensor-proxy --since "1 hour ago" -o json | \
jq -r 'select(.corr_id != null) | [.corr_id, .uid, .method, .node] | @tsv'
# Failed SSH requests
journalctl -u pulse-sensor-proxy --since today | grep -E '(SSH.*failed|error)'
# Rate limit hits
journalctl -u pulse-sensor-proxy --since today | grep "rate limit"
# Specific correlation ID
journalctl -u pulse-sensor-proxy | grep "corr_id=a7f3d"
```
### Rate Limiting
**Current limits (per peer UID+PID):**
- **Rate**: 20 requests/minute (token bucket with burst)
- **Burst**: 10 requests
- **Concurrency**: 10 simultaneous requests
**Behavior on limit exceeded:**
- Request rejected immediately (no queuing)
- `pulse_proxy_rate_limit_hits_total` metric incremented
- Log entry: `"Rate limit exceeded"`
- HTTP-like semantics: Similar to 429 Too Many Requests
**Adjust limits:**
Limits are hardcoded in `throttle.go`. To adjust, modify and rebuild:
```go
// cmd/pulse-sensor-proxy/throttle.go
const (
requestsPerMin = 20 // Change this
requestBurst = 10 // Change this
maxConcurrent = 10 // Change this
)
```
Then rebuild and restart:
```bash
go build -v ./cmd/pulse-sensor-proxy
systemctl restart pulse-sensor-proxy
```
## Incident Response
### Suspected Compromise Checklist
**If the proxy or host is suspected compromised:**
1. **Isolate immediately:**
```bash
# Stop proxy service
systemctl stop pulse-sensor-proxy
# Block outbound SSH from host (if applicable)
iptables -A OUTPUT -p tcp --dport 22 -j REJECT
```
2. **Rotate all keys:**
```bash
# Remove compromised keys from all nodes
for node in pve1 pve2 pve3; do
ssh root@$node "sed -i '/pulse-sensor-proxy/d' /root/.ssh/authorized_keys"
done
# Generate new keys (don't use rotation script - may be compromised)
rm -rf /var/lib/pulse-sensor-proxy/ssh*
mkdir -p /var/lib/pulse-sensor-proxy/ssh
ssh-keygen -t ed25519 -N '' -C "pulse-sensor-proxy emergency $(date -u +%Y%m%dT%H%M%SZ)" \
-f /var/lib/pulse-sensor-proxy/ssh/id_ed25519
chown -R pulse-sensor-proxy:pulse-sensor-proxy /var/lib/pulse-sensor-proxy/ssh
chmod 0700 /var/lib/pulse-sensor-proxy/ssh
chmod 0600 /var/lib/pulse-sensor-proxy/ssh/id_ed25519
chmod 0640 /var/lib/pulse-sensor-proxy/ssh/id_ed25519.pub
```
3. **Audit logs:**
```bash
# Export all proxy logs
journalctl -u pulse-sensor-proxy --since "7 days ago" > /tmp/proxy-audit-$(date +%s).log
# Look for anomalies:
# - Unusual correlation IDs
# - High rate limit hits
# - Unexpected UIDs/PIDs
# - SSH errors to unexpected nodes
```
4. **Reinstall proxy:**
```bash
# Re-run installation script
/opt/pulse/scripts/install-sensor-proxy.sh
# Verify service status
systemctl status pulse-sensor-proxy
```
5. **Re-push keys:**
```bash
# Use proxy RPC to push new keys
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh
```
6. **Verify no persistence mechanisms:**
```bash
# Check for unexpected systemd units
systemctl list-units --all | grep -i proxy
# Check for unexpected cron jobs
crontab -l -u pulse-sensor-proxy
# Check for unauthorized files in /var/lib/pulse-sensor-proxy
find /var/lib/pulse-sensor-proxy -type f ! -path '*/ssh/*' ! -path '*/ssh.d/*'
```
### Post-Incident Hardening
After an incident, consider:
- **Audit all LXC containers** for unexpected privilege escalation
- **Review bind mounts** on all containers (check for unauthorized mounts)
- **Enable full syscall auditing** (`auditd`) on host
- **Restrict network access** to proxy metrics endpoint (firewall `127.0.0.1:9127`)
- **Implement log aggregation** (forward `journald` to central SIEM)
## Testing & Rollout
### Development Testing
Before deploying to production, verify the implementation with these safe tests:
**1. Build Verification:**
```bash
# Compile proxy
cd /opt/pulse
go build -v ./cmd/pulse-sensor-proxy
# Verify binary
./pulse-sensor-proxy version
# Expected: pulse-sensor-proxy dev (or version number)
# Check help output
./pulse-sensor-proxy --help
```
**2. Rotation Script Syntax:**
```bash
# Syntax check
bash -n /opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh
# Help output
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --help
# Dry-run (requires root and socket)
sudo /opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --dry-run
```
**3. Configuration Validation:**
```bash
# Test config file parsing
cat > /tmp/test-config.yaml <<EOF
allowed_source_subnets:
- "192.168.0.0/24"
- "10.0.0.0/8"
metrics_address: "127.0.0.1:9127"
EOF
# Validate YAML syntax
python3 -c "import yaml; yaml.safe_load(open('/tmp/test-config.yaml'))"
```
### Production Rollout Checklist
**Phase 1: Pre-Deployment (on host)**
1. **Backup current state:**
```bash
# Backup systemd unit
cp /etc/systemd/system/pulse-sensor-proxy.service \
/etc/systemd/system/pulse-sensor-proxy.service.backup
# Backup SSH keys
tar -czf /tmp/pulse-sensor-proxy-keys-backup-$(date +%s).tar.gz \
/var/lib/pulse-sensor-proxy/ssh/
# Note current service status
systemctl status pulse-sensor-proxy > /tmp/pulse-sensor-proxy-status-before.txt
```
2. **Create service account:**
```bash
# Run install script or manually create
if ! id -u pulse-sensor-proxy >/dev/null 2>&1; then
useradd --system --user-group --no-create-home --shell /usr/sbin/nologin pulse-sensor-proxy
fi
```
3. **Update file ownership:**
```bash
chown -R pulse-sensor-proxy:pulse-sensor-proxy /var/lib/pulse-sensor-proxy/
chmod 0750 /var/lib/pulse-sensor-proxy/
chmod 0700 /var/lib/pulse-sensor-proxy/ssh/
chmod 0600 /var/lib/pulse-sensor-proxy/ssh/id_ed25519
chmod 0640 /var/lib/pulse-sensor-proxy/ssh/id_ed25519.pub
```
**Phase 2: Deploy Hardened Version**
1. **Build and install binary:**
```bash
cd /opt/pulse
go build -v -o /tmp/pulse-sensor-proxy ./cmd/pulse-sensor-proxy
# Verify build
/tmp/pulse-sensor-proxy version
# Install
sudo install -m 0755 -o root -g root /tmp/pulse-sensor-proxy /usr/local/bin/pulse-sensor-proxy
```
2. **Install hardened systemd unit:**
```bash
# Copy hardened unit
sudo cp /opt/pulse/scripts/pulse-sensor-proxy.service /etc/systemd/system/
# Verify syntax
systemd-analyze verify /etc/systemd/system/pulse-sensor-proxy.service
# Reload systemd
sudo systemctl daemon-reload
```
3. **Update RuntimeDirectoryMode for LXC access:**
```bash
# Ensure socket directory is accessible from container
sudo mkdir -p /etc/systemd/system/pulse-sensor-proxy.service.d/
cat | sudo tee /etc/systemd/system/pulse-sensor-proxy.service.d/lxc-access.conf <<'EOF'
[Service]
RuntimeDirectoryMode=0775
EOF
sudo systemctl daemon-reload
```
**Phase 3: Restart and Verify**
1. **Restart service:**
```bash
sudo systemctl restart pulse-sensor-proxy
# Check status
sudo systemctl status pulse-sensor-proxy
```
2. **Verify service user:**
```bash
ps aux | grep pulse-sensor-proxy | grep -v grep
# Expected: pulse-sensor-proxy user, not root
```
3. **Check socket permissions:**
```bash
ls -ld /run/pulse-sensor-proxy/
# Expected: drwxrwxr-x pulse-sensor-proxy pulse-sensor-proxy
ls -l /run/pulse-sensor-proxy/pulse-sensor-proxy.sock
# Expected: srwxrwxrwx pulse-sensor-proxy pulse-sensor-proxy
```
4. **Test from container:**
```bash
# Inside LXC container running Pulse
ls -la /run/pulse-sensor-proxy/
# Should show socket
# Check Pulse logs for connection success
journalctl -u pulse-backend -n 50 | grep -i temperature
```
**Phase 4: End-to-End Validation**
1. **Test RPC methods:**
```bash
# On host, test socket connectivity
echo '{"correlation_id":"test-001","method":"register_nodes","params":{}}' | \
sudo socat - UNIX-CONNECT:/run/pulse-sensor-proxy/pulse-sensor-proxy.sock | jq .
# Should return cluster nodes list
```
2. **Test temperature fetch:**
```bash
# From container or via socket
echo '{"correlation_id":"test-002","method":"get_temp","params":{"node":"pve1"}}' | \
socat - UNIX-CONNECT:/run/pulse-sensor-proxy/pulse-sensor-proxy.sock | jq .
# Should return sensors JSON data
```
3. **Verify metrics endpoint:**
```bash
curl -s http://127.0.0.1:9127/metrics | grep pulse_proxy
# Should show metrics like:
# pulse_proxy_rpc_requests_total{method="get_temp",result="success"} N
# pulse_proxy_queue_depth 0
```
4. **Test SSH key rotation:**
```bash
# Dry-run first
sudo /opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --dry-run
# Full rotation (if confident)
sudo /opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh
# Verify all nodes updated
for node in pve1 pve2 pve3; do
ssh root@$node "tail -1 /root/.ssh/authorized_keys"
done
```
5. **Audit logging verification:**
```bash
# Check logs include correlation IDs and peer credentials
sudo journalctl -u pulse-sensor-proxy --since "5 minutes ago" -o json | \
jq -r 'select(.corr_id != null) | [.corr_id, .uid, .method] | @tsv'
# Should show structured logging with UIDs
```
**Phase 5: Monitoring Setup**
1. **Configure Prometheus scraping:**
```yaml
# Add to prometheus.yml
scrape_configs:
- job_name: 'pulse-sensor-proxy'
static_configs:
- targets: ['localhost:9127']
```
2. **Import alert rules:**
```bash
# Copy alert rules from docs to Prometheus alerts directory
# Reload Prometheus configuration
```
3. **Verify alerts fire (optional stress test):**
```bash
# Generate rate limit hits (test alert)
for i in {1..50}; do
echo '{"correlation_id":"stress-'$i'","method":"register_nodes","params":{}}' | \
socat - UNIX-CONNECT:/run/pulse-sensor-proxy/pulse-sensor-proxy.sock &
done
wait
# Check rate limit metric increased
curl -s http://127.0.0.1:9127/metrics | grep rate_limit_hits
```
### Rollback Procedure
If issues occur during rollout:
1. **Stop new service:**
```bash
sudo systemctl stop pulse-sensor-proxy
```
2. **Restore backup:**
```bash
sudo cp /etc/systemd/system/pulse-sensor-proxy.service.backup \
/etc/systemd/system/pulse-sensor-proxy.service
sudo systemctl daemon-reload
```
3. **Restore SSH keys (if rotated):**
```bash
# If rotation was performed and failed
sudo /opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --rollback
```
4. **Restart with old configuration:**
```bash
sudo systemctl restart pulse-sensor-proxy
sudo systemctl status pulse-sensor-proxy
```
5. **Verify Pulse connectivity:**
```bash
# Check Pulse can still fetch temperatures
# Monitor Pulse logs
```
### Known Limitations
- **No automated unit tests**: Code verification relies on build success and manual testing
- **Key rotation requires manual trigger**: Automated timer setup is optional
- **Metrics require Prometheus**: No built-in alerting without external monitoring
- **LXC bind mount required**: Container must have directory-level bind mount configured
- **Root required for rotation script**: Script needs root to run `ensure_cluster_keys` RPC
### Future Improvements
- Add Go unit tests for validation, throttling, and metrics logic
- Implement health check endpoint (e.g., `/health`) separate from metrics
- Add support for TLS on metrics endpoint
- Create automated integration test suite
- Add `--check` flag to rotation script for pre-flight validation
- Support for multiple LXC containers accessing same proxy instance
## Appendix
### Quick Verification Checklist
**Host:**
- [ ] Service running as `pulse-sensor-proxy` user (not root)
- [ ] Keys in `/var/lib/pulse-sensor-proxy/ssh/` owned by `pulse-sensor-proxy:pulse-sensor-proxy`
- [ ] Private key permissions: `0600`
- [ ] Socket directory permissions: `0775` (not `0770`)
- [ ] Metrics endpoint accessible: `curl http://127.0.0.1:9127/metrics`
**Container:**
- [ ] Container is unprivileged (`unprivileged: 1` in config)
- [ ] Bind mount exists: `ls /run/pulse-sensor-proxy/pulse-sensor-proxy.sock`
- [ ] AppArmor enforced: `cat /proc/self/attr/current` shows confinement
- [ ] Pulse can connect to socket (check Pulse logs)
**SSH Keys:**
- [ ] All nodes have `pulse-sensor-proxy` key in `/root/.ssh/authorized_keys`
- [ ] Keys include `from="..."` restrictions
- [ ] Keys include `command="sensors -j"` forced command
- [ ] Keys include `no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty`
**Monitoring:**
- [ ] Prometheus scraping metrics successfully
- [ ] Alerts configured for SSH failures, rate limiting, queue depth
- [ ] Logs forwarded to central logging (optional but recommended)
### Reference Commands
**Service Management:**
```bash
systemctl status pulse-sensor-proxy # Check service status
systemctl restart pulse-sensor-proxy # Restart service
journalctl -u pulse-sensor-proxy -f # Tail logs
```
**Key Management:**
```bash
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --dry-run # Dry-run rotation
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh # Perform rotation
/opt/pulse/scripts/pulse-sensor-proxy-rotate-keys.sh --rollback # Rollback
```
**Metrics:**
```bash
curl http://127.0.0.1:9127/metrics # Fetch all metrics
curl -s http://127.0.0.1:9127/metrics | grep pulse_proxy # Filter proxy metrics
```
**Manual RPC (Testing):**
```bash
# Using socat (inline JSON)
echo '{"correlation_id":"test","method":"get_temp","params":{"node":"pve1"}}' | \
socat - UNIX-CONNECT:/run/pulse-sensor-proxy/pulse-sensor-proxy.sock
# Using Python (proper JSON-RPC client)
python3 <<'PY'
import json, socket, uuid
payload = {
"correlation_id": str(uuid.uuid4()),
"method": "get_temp",
"params": {"node": "pve1"}
}
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect("/run/pulse-sensor-proxy/pulse-sensor-proxy.sock")
s.sendall((json.dumps(payload) + "\n").encode())
s.shutdown(socket.SHUT_WR)
print(s.recv(65536).decode())
PY
```
**Verification:**
```bash
# Check service user
ps aux | grep pulse-sensor-proxy | grep -v grep
# Check file ownership
ls -lR /var/lib/pulse-sensor-proxy/
# Check bind mount in container
pct enter <VMID>
ls -la /run/pulse-sensor-proxy/
# Check SSH keys on nodes
for node in pve1 pve2 pve3; do
echo "=== $node ==="
ssh root@$node "grep pulse-sensor-proxy /root/.ssh/authorized_keys"
done
```
---
**Document Version:** 1.0
**Last Updated:** 2025-10-13
**Applies To:** pulse-sensor-proxy v1.0+