Relay Management¶
Relays are the bridge between the Stentor server and target environments. They run on attack infrastructure (typically Kali Linux) and host C2 listeners, generate payloads, process implant check-ins, and route traffic via WebSocket back to the server. This guide covers building, deploying, registering, monitoring, and troubleshooting relay agents.
Architecture¶
The relay sits between the Stentor backend and the target network. It maintains a persistent WebSocket connection to the server for command dispatch, and runs local C2 listeners that implants connect to.
graph LR
subgraph Backend
A[Stentor Server<br/>:8082]
end
subgraph Relay["Relay (Kali)"]
B[WebSocket Client]
C[C2 Listener<br/>HTTP/HTTPS]
D[DNS C2 Server]
E[SMB Pipe Server]
F[Payload Generator]
end
subgraph Target["Target Network"]
G[Implant<br/>Windows]
end
A <-->|WebSocket| B
G -->|HTTPS| C
G -->|DNS queries| D
G -->|SMB named pipe| E
C -->|Events| B
B -->|Commands| C Traffic flow:
- The operator issues a command via the Stentor UI or API.
- The backend sends a WebSocket command to the relay (e.g.,
queue_task,start_listener,generate_payload). - The relay executes the command locally -- starts a listener, queues a task for a beacon, or generates a payload binary.
- When an implant checks in to a relay-hosted listener, the relay sends a WebSocket event back to the backend (e.g.,
beacon_new,beacon_checkin,task_complete). - The backend updates its database and pushes real-time updates to the operator UI via the CockpitHub WebSocket.
Building the Relay¶
Build the relay binary from the project root. The relay is a single static Go binary with no external dependencies.
# From the Stentor project root
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
-ldflags="-s -w -X main.Version=1.1.0" \
-o binaries/stentor-relay \
./relay/cmd/relay/
Build flags explained:
| Flag | Purpose |
|---|---|
GOOS=linux | Cross-compile for Linux (relay runs on Kali) |
GOARCH=amd64 | Target 64-bit x86 architecture |
CGO_ENABLED=0 | Static binary with no C library dependencies |
-s -w | Strip debug info and DWARF symbols (smaller binary) |
-X main.Version=1.1.0 | Embed version string for identification |
Version identification
The relay logs its version on startup. Use meaningful version strings to track which build is deployed: Stentor Kali Relay Agent starting... Version: 1.1.0
Deployment¶
Transfer to Relay Host¶
The relay binary is typically deployed via SCP through a jump host (Proxmox) to the Kali relay VM:
# Step 1: Dev machine -> Proxmox host
scp -o StrictHostKeyChecking=no binaries/stentor-relay \
root@<proxmox-host>:/tmp/stentor-relay
# Step 2: Proxmox host -> Kali relay
ssh root@<proxmox-host> 'scp -o StrictHostKeyChecking=no \
/tmp/stentor-relay root@<kali-ip>:/opt/stentor-relay/stentor-relay'
Lab deployment example
# Using the default lab infrastructure
scp binaries/stentor-relay root@<proxmox-ip>:/tmp/stentor-relay
ssh root@<proxmox-ip> 'sshpass -p "<relay-password>" scp -o StrictHostKeyChecking=no \
/tmp/stentor-relay [email protected]:/opt/stentor-relay/stentor-relay'
Configuration¶
The relay is configured entirely via environment variables, typically stored in /opt/stentor-relay/.env. The relay reads these on startup through the systemd EnvironmentFile directive.
Required Variables¶
| Variable | Description | Example |
|---|---|---|
BACKEND_URL | Server WebSocket endpoint | wss://stentor.app/ws/relay |
RELAY_ID | Relay UUID (must match database record) | aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee |
RELAY_SECRET | Shared secret for WebSocket authentication | <random-string> |
C2 Listener Variables¶
| Variable | Default | Description |
|---|---|---|
C2_PORT | 443 | Port for the default HTTPS C2 listener |
C2_CERT_PATH | -- | TLS certificate for HTTPS listeners |
C2_KEY_PATH | -- | TLS private key for HTTPS listeners |
C2_STAGING_AUTH_TOKEN | -- | Bearer token for staging endpoint (empty = unauthenticated) |
Optional Service Variables¶
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | info | Logging verbosity: debug, info, warn, error |
RECONNECT_INTERVAL_SECONDS | 5 | Delay between WebSocket reconnection attempts |
HEARTBEAT_INTERVAL_SECONDS | 30 | Heartbeat frequency to the backend |
SMTP_PORT | 25 | Embedded SMTP server port (0 = disabled) |
SMTP_EXTERNAL_HOST | -- | External SMTP relay for forwarding |
SMTP_EXTERNAL_PORT | 587 | External SMTP relay port |
TRACKING_PORT | 80 | HTTP tracking server port (0 = disabled) |
DNS_PORT | 0 | DNS C2 server port (0 = disabled) |
DNS_DOMAIN | -- | Domain suffix for DNS C2 queries (required if DNS_PORT > 0) |
DNS_TTL | 60 | TTL for DNS C2 responses in seconds |
PROFILER_PORT | 0 | System Profiler HTTP server port (0 = disabled) |
HEALTH_PORT | 9090 | HTTP health endpoint port (0 = disabled) |
STATE_DIR | /var/lib/stentor-relay | Listener state persistence directory |
DNS C2 requirement
If DNS_PORT is set to a non-zero value, DNS_DOMAIN must also be configured. The relay will fail to start if DNS port is enabled without a domain.
Example .env file:
# Required
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
RELAY_SECRET=your-shared-secret-here
# C2 listener
C2_PORT=8443
C2_CERT_PATH=/opt/stentor-relay/certs/cert.pem
C2_KEY_PATH=/opt/stentor-relay/certs/key.pem
# Optional services
LOG_LEVEL=info
SMTP_PORT=0
TRACKING_PORT=0
DNS_PORT=0
PROFILER_PORT=0
Systemd Service¶
Create a systemd service for automatic startup and restart on failure.
Service file (/etc/systemd/system/stentor-relay.service):
[Unit]
Description=Stentor Relay Agent
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/stentor-relay
ExecStart=/opt/stentor-relay/stentor-relay
EnvironmentFile=/opt/stentor-relay/.env
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Service management commands:
# Install and enable the service
sudo systemctl daemon-reload
sudo systemctl enable stentor-relay
# Start the relay
sudo systemctl start stentor-relay
# Check status
sudo systemctl status stentor-relay
# View logs (real-time)
journalctl -u stentor-relay -f
# View recent logs
journalctl -u stentor-relay --since "1 hour ago"
# Restart after configuration change
sudo systemctl restart stentor-relay
Startup verification
On successful startup, the relay logs its configuration and connection status:
Registration¶
Before a relay can connect via WebSocket, it must exist in the Stentor database. The WebSocket handler at /ws/relay calls relayRepo.GetByID() and returns a 404 Not Found if the relay UUID is not in the relays table.
Critical: Register before connecting
The relay must be registered in the database before starting the relay service. If the relay is not found in the database, the WebSocket handshake will fail with a 404 error and the relay will repeatedly fail to connect.
Via API¶
Create a relay record through the REST API. The response includes the generated id -- use this as the RELAY_ID in the relay's .env file.
Via Direct SQL¶
Useful after a database rebuild (e.g., --rebuild-db deployment) when you need to re-register a relay with its existing UUID:
INSERT INTO relays (id, name, description, ip_address, status)
VALUES (
'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
'Kali Relay',
'Primary attack relay',
'10.0.0.50',
'online'
)
ON CONFLICT (id) DO NOTHING;
Execute via Docker:
ssh root@<proxmox> 'ssh [email protected] \
"docker exec -i stentor-db psql -U stentor -d stentor_db"' <<< \
"INSERT INTO relays (id, name, description, ip_address, status) \
VALUES ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'Kali Relay', \
'Primary attack relay', '10.0.0.50', 'online') \
ON CONFLICT (id) DO NOTHING;"
After registration, restart the relay service for it to establish the WebSocket connection:
WebSocket Protocol¶
The relay communicates with the backend over a persistent WebSocket connection at /ws/relay. Authentication uses custom headers.
Authentication¶
The relay authenticates using two headers on the WebSocket upgrade request:
| Header | Description |
|---|---|
X-Relay-ID | Relay UUID matching a database record |
X-Relay-Secret | Shared secret matching the server's RELAY_SECRET |
Both headers can alternatively be passed as query parameters (relay_id and secret).
mTLS alternative
Relays can also authenticate using TLS client certificates (mTLS) instead of shared secret headers. See mTLS Authentication for setup instructions.
Message Types¶
All messages follow a JSON envelope format with type, id, correlation_id, timestamp, and payload fields.
| Type | Direction | Description |
|---|---|---|
command | Server -> Relay | Initiates actions (start listener, queue task, generate payload) |
response | Relay -> Server | Returns command results with correlation ID |
event | Relay -> Server | Async notifications (beacon new, task complete, listener status) |
heartbeat | Bidirectional | Connection health monitoring |
ack | Relay -> Server | Acknowledges command receipt before execution completes |
Key Commands (Server -> Relay)¶
| Command | Description |
|---|---|
start_listener | Start a C2 listener on the relay |
stop_listener | Stop a running listener |
queue_task | Queue a task for delivery to a beacon |
generate_payload | Build an implant binary (EXE, DLL, shellcode, etc.) |
send_email | Send a phishing email via the relay's SMTP server |
tunnel | Forward SOCKS tunnel frames to a beacon |
host_file | Host a file at a custom URI on a listener |
unhost_file | Remove a hosted file from a listener |
Key Events (Relay -> Server)¶
| Event | Description |
|---|---|
beacon_new | New implant registered |
beacon_checkin | Existing beacon checked in |
beacon_dead | Beacon missed check-ins and marked dead |
task_complete | Task execution completed with results |
listener_started | Listener successfully started |
listener_stopped | Listener shut down |
listener_error | Listener encountered an error |
Full protocol reference
For complete message format documentation, field definitions, and sequence diagrams, see WebSocket Protocol.
Health Monitoring¶
Relay Status via API¶
Check the status of all registered relays:
curl -s https://stentor.app/api/v1/relays \
-H "Authorization: Bearer $TOKEN" | jq '.[] | {id, name, status}'
The status field reflects the WebSocket connection state:
| Status | Meaning |
|---|---|
connected | Relay has an active WebSocket connection |
disconnected | Relay was previously connected but the connection dropped |
offline | Relay has never connected (freshly registered) |
/healthz Endpoint¶
Each relay exposes an HTTP health endpoint on a dedicated port (default: 9090, configurable via HEALTH_PORT). This endpoint runs on a separate port from C2 listeners to avoid conflicts and is accessible without authentication.
Response fields:
{
"status": "healthy",
"uptime": 86400,
"active_listeners": 2,
"connected_beacons": 5,
"last_checkin": "2025-06-15T14:30:00Z",
"version": "1.1.0",
"timestamp": "2025-06-15T14:31:00Z"
}
| Field | Type | Description |
|---|---|---|
status | string | Always "healthy" when the endpoint responds |
uptime | int64 | Seconds since relay process started |
active_listeners | int | Count of currently running C2 listeners |
connected_beacons | int | Count of beacons registered with this relay |
last_checkin | string | RFC 3339 timestamp of the most recent beacon check-in (omitted if no beacons have checked in) |
version | string | Relay binary version string |
timestamp | string | Current UTC time in RFC 3339 format |
Disabling the health endpoint
Set HEALTH_PORT=0 in the relay's .env file to disable the /healthz endpoint entirely. This may be useful on relays where network exposure must be minimized.
Server-Side Health Polling¶
The Stentor server automatically polls the /healthz endpoint on all connected relays to keep health data up to date.
Polling behavior:
- The server polls every 30 seconds for all relays with status
connectedand a non-empty IP address. - Uses a standard HTTP GET request (not the WebSocket channel) for simplicity and isolation.
- Each poll request has a 5-second timeout to avoid blocking on unresponsive relays.
- Health data is persisted to the database using nullable fields --
nilvalues indicate the relay has not yet been polled.
No configuration required
Server-side health polling starts automatically when the backend launches. There are no server-side environment variables to configure -- the server always polls on port 9090.
Health Status in UI¶
The relay selector in the Stentor UI displays a health indicator dot next to each relay name:
| Indicator | Condition | Meaning |
|---|---|---|
| Health data received within 60 seconds | Relay is healthy and responsive | |
| Health data received within 5 minutes | Relay may be experiencing issues | |
| Health data older than 5 minutes or not yet received | Relay is stale or has never reported health |
The UI also shows the beacon count and listener count alongside the relay name when health data is available.
WebSocket Heartbeat¶
The relay sends periodic heartbeat messages to the backend at the configured interval (default: every 30 seconds). The heartbeat payload includes the relay ID and current status (idle, busy, or error).
If the backend does not receive a heartbeat within the expected interval, it marks the relay as potentially disconnected. The RelayHub automatically updates the relay status in the database when connections are established or lost.
Heartbeat vs. health polling
The WebSocket heartbeat monitors the connection between relay and server (is the WebSocket alive?). The /healthz poll monitors the relay's operational state (how many listeners, beacons, uptime). Both are independent mechanisms.
Log Monitoring¶
Monitor relay health in real-time via systemd journal:
# Real-time log stream
journalctl -u stentor-relay -f
# Filter for errors only
journalctl -u stentor-relay -p err
# Logs from the last boot
journalctl -u stentor-relay -b
Key log messages to watch for:
| Log Message | Meaning |
|---|---|
Relay Agent ready, awaiting commands | Successful startup |
C2 server listening on :8443 | C2 listener active |
WebSocket client error | Connection to backend failed |
Received signal SIGTERM, shutting down | Graceful shutdown initiated |
Command X completed successfully | Task executed |
Command X failed | Task execution error |
Beacon Failover & Relay Resilience¶
Stentor supports multi-relay failover for beacon resilience. If a relay goes down, beacons can automatically switch to a backup relay and transparently recover to the primary when it returns.
Failover URL List¶
Beacons can be built with multiple C2 URLs. The first URL is always the primary; additional URLs are backup relays. Single-URL deployments are completely unaffected -- failover is backward compatible.
Configure failover URLs at build time via environment variable or linker flag:
# Environment variable (comma-separated)
IMPLANT_C2_URLS=https://relay1.example.com:8443,https://relay2.example.com:8443
# Or via Go ldflags at build time
-ldflags="-X main.DefaultC2URLs=https://relay1.example.com:8443,https://relay2.example.com:8443"
Failover Behavior¶
When a beacon encounters consecutive failures communicating with its current relay, it automatically switches to the next URL in the list:
| Parameter | Default | Description |
|---|---|---|
MaxFailures | 3 | Consecutive failure count before switching to the next relay |
After 3 consecutive failures on the current host, the beacon advances to the next URL in the failover list. If the last URL also fails, it wraps around to the first.
graph LR
B[Beacon] -->|3 failures| R1[Relay 1<br/>Primary]
R1 -.->|down| X1[X]
B -->|switch| R2[Relay 2<br/>Backup]
R2 -->|working| OK[Continue] Primary-Preference Recovery¶
While operating on a backup relay, the beacon periodically probes the primary to check if it has recovered. This ensures beacons always return to the preferred relay when possible.
| Parameter | Default | Description |
|---|---|---|
PrimaryCheckInterval | 5 | Successful requests on a backup before probing the primary |
PrimaryCheckTimeout | 5s | Timeout for the primary recovery probe |
Recovery flow:
- Beacon is running on a backup relay after failover.
- After 5 successful requests on the backup, the beacon sends a lightweight probe (minimal check-in) to the primary URL.
- If the primary responds within 5 seconds, the beacon transparently switches back to the primary.
- If the primary is still down, the beacon stays on the backup and retries after another 5 successful requests.
Transparent to operators
Failover and recovery happen automatically without operator intervention. The beacon continues executing tasks normally throughout the process. Operators can monitor which relay a beacon is connected to via the Cockpit UI.
Listener State Persistence¶
Relays persist all active listener configurations to disk so they can automatically restore listeners after a restart or crash -- no operator intervention required.
How it works:
- Listener state is saved to a JSON file in the
STATE_DIRdirectory (default:/var/lib/stentor-relay). - Uses atomic writes (write to temp file, then rename) for crash safety -- partial writes never corrupt the state file.
- State is saved after each listener start or stop, with a 200ms delay to confirm the operation succeeded.
- On restart, the relay reads the state file and automatically re-starts all previously active listeners.
Configuration:
Directory permissions
The STATE_DIR directory must be writable by the relay process. If running as root (typical for Kali deployments), the default /var/lib/stentor-relay works out of the box. For non-root deployments, ensure the directory exists and has appropriate permissions.
Failover Configuration Example¶
A complete failover deployment with two relays and listener state persistence:
# Beacon build with failover URLs (primary + backup)
IMPLANT_C2_URLS=https://relay1.example.com:8443,https://relay2.example.com:8443
# Relay 1 .env (primary)
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-1111-1111-1111-111111111111
RELAY_SECRET=shared-secret
STATE_DIR=/var/lib/stentor-relay
HEALTH_PORT=9090
# Relay 2 .env (backup)
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=bbbbbbbb-2222-2222-2222-222222222222
RELAY_SECRET=shared-secret
STATE_DIR=/var/lib/stentor-relay
HEALTH_PORT=9090
mTLS Authentication¶
mTLS (mutual TLS) adds per-relay client certificate authentication to the WebSocket connection. It provides cryptographic identity verification as an alternative to shared secret headers.
Overview¶
mTLS is additive -- when configured, the relay presents a client certificate during the TLS handshake. The server verifies the certificate against a trusted CA and extracts the relay UUID from the certificate's Common Name (CN). Shared secret authentication remains available as a fallback for backward compatibility.
flowchart TD
R[Relay connects to server] --> TLS{Client cert<br/>present?}
TLS -->|Yes| V[Verify cert against CA pool]
V --> CN[Extract relay UUID from CN]
CN --> AUTH1[Authenticated via mTLS]
TLS -->|No| REQ{RELAY_MTLS_REQUIRED<br/>= true?}
REQ -->|Yes| REJECT[Reject connection]
REQ -->|No| SECRET[Check X-Relay-Secret header]
SECRET --> AUTH2[Authenticated via shared secret] Certificate Generation¶
Generate a CA and relay client certificate using OpenSSL. The certificate CN must follow the format relay-{uuid} -- the server extracts the relay UUID from this field.
# 1. Generate CA key and certificate (one-time setup)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=Stentor Relay CA"
# 2. Generate relay client key and CSR
# CN must be "relay-{uuid}" matching the relay's RELAY_ID
openssl genrsa -out relay.key 4096
openssl req -new -key relay.key -out relay.csr \
-subj "/CN=relay-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
# 3. Sign the relay certificate with the CA
openssl x509 -req -in relay.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out relay.crt -days 365
CN format is critical
The Common Name must be exactly relay-{uuid} (e.g., relay-25b2e4d7-8b7f-411f-8fe0-7faed06b90d1). The server parses this field to extract the relay UUID for identity verification. An incorrect CN format will cause authentication to fail with ErrInvalidCertCN.
Server Configuration¶
Configure the Stentor server to accept mTLS connections from relays:
| Variable | Default | Description |
|---|---|---|
RELAY_CA_CERT_PATH | -- | Path to CA certificate PEM file for verifying relay client certs |
RELAY_MTLS_REQUIRED | false | Set to true to reject shared-secret auth and enforce mTLS-only |
The server loads the CA certificate pool on startup and uses it to verify the certificate chain presented by connecting relays. It also checks certificate time validity (not expired, not yet valid) and verifies the ExtKeyUsageClientAuth extended key usage.
Relay Configuration¶
Configure the relay to present a client certificate when connecting to the server:
| Variable | Default | Description |
|---|---|---|
MTLS_CERT_PATH | -- | Path to relay client certificate PEM |
MTLS_KEY_PATH | -- | Path to relay client private key PEM |
MTLS_CA_CERT_PATH | -- | Optional CA cert for verifying server certificate |
Both cert and key required
MTLS_CERT_PATH and MTLS_KEY_PATH must both be set. Setting only one will cause a configuration validation error on relay startup.
When MTLS_CA_CERT_PATH is not set on the relay side, it uses the system certificate store (or InsecureSkipVerify in development environments with self-signed server certificates).
Example relay .env with mTLS:
# Required
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
RELAY_SECRET=your-shared-secret-here
# mTLS authentication
MTLS_CERT_PATH=/opt/stentor-relay/certs/relay.crt
MTLS_KEY_PATH=/opt/stentor-relay/certs/relay.key
MTLS_CA_CERT_PATH=/opt/stentor-relay/certs/ca.crt
Migration from Shared Secret to mTLS¶
Follow these steps to migrate an existing relay from shared-secret authentication to mTLS without downtime:
Step 1: Generate certificates
# Generate CA and relay certificates (see Certificate Generation above)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=Stentor Relay CA"
openssl genrsa -out relay.key 4096
openssl req -new -key relay.key -out relay.csr \
-subj "/CN=relay-<your-relay-uuid>"
openssl x509 -req -in relay.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out relay.crt -days 365
Step 2: Deploy CA cert to server
Copy ca.crt to the server and set RELAY_CA_CERT_PATH in the server's .env. Restart the backend.
Step 3: Deploy client cert/key to relay
Copy relay.crt and relay.key to the relay host (e.g., /opt/stentor-relay/certs/). Set MTLS_CERT_PATH and MTLS_KEY_PATH in the relay's .env.
Step 4: Restart relay
The relay will now authenticate via mTLS. The shared secret (X-Relay-Secret header) remains as a fallback.
Step 5: Verify mTLS is working
Check server logs for mTLS authentication messages:
Step 6: Enforce mTLS-only (optional)
Once all relays are using mTLS, optionally disable shared-secret authentication:
Restart the backend. Relays without valid client certificates will no longer be able to connect.
Test before enforcing
Only set RELAY_MTLS_REQUIRED=true after confirming all relays are successfully authenticating via mTLS. Setting this flag with misconfigured relays will lock them out.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| 404 on WebSocket connect | Relay UUID not in database | Register the relay via API or direct SQL before starting the service. Verify RELAY_ID in .env matches the database record. |
| 401 Unauthorized | Wrong RELAY_SECRET | Verify the relay's RELAY_SECRET matches the server's .env configuration. Both sides must use the same shared secret. |
| Connection drops repeatedly | Network instability or WebSocket buffer overflow | Check relay logs with journalctl -u stentor-relay -f. Verify network connectivity to the backend. The relay auto-reconnects every 5 seconds by default. |
| Listener fails to start | Port already in use or permission denied | Check for conflicting processes: ss -tlnp \| grep <port>. Use sudo or ports >= 1024 to avoid permission issues. |
| Payload generation fails | Missing Go toolchain or disk space | Verify Go is installed on the relay: go version. Check available disk: df -h /opt/stentor-relay. |
| Beacon not appearing | Listener not started or network ACL | Verify listener status via API: GET /api/v1/listeners. Check firewall rules between target and relay IP. |
| "configuration validation failed" | Missing or invalid environment variables | Check the relay log for the specific validation error. Ensure all required variables (BACKEND_URL, RELAY_ID) are set. Verify BACKEND_URL starts with ws:// or wss://. |
| DNS C2 not responding | DNS port conflict or missing domain | Ensure DNS_DOMAIN is set when DNS_PORT > 0. Check for port conflicts: ss -ulnp \| grep <dns-port>. |
| SMTP send fails | External relay misconfigured | Verify SMTP_EXTERNAL_HOST, SMTP_EXTERNAL_PORT, and credentials. Test SMTP connectivity from the relay host. |
Diagnostic Commands¶
# Check relay process
systemctl status stentor-relay
# View relay configuration (from logs)
journalctl -u stentor-relay | head -20
# Test WebSocket connectivity
curl -s -o /dev/null -w "%{http_code}" \
-H "X-Relay-ID: <relay-id>" \
-H "X-Relay-Secret: <secret>" \
https://stentor.app/ws/relay
# Check listening ports on relay
ss -tlnp | grep stentor
# Verify network path to target
ping -c 3 <target-ip>
# Check TLS certificate expiry
openssl x509 -in /opt/stentor-relay/certs/cert.pem -noout -dates
Multi-Relay Architecture¶
Stentor supports multiple simultaneous relay connections for segmented or distributed operations.
graph TB
subgraph Backend
S[Stentor Server]
end
subgraph External["External Network"]
R1[Relay 1<br/>Internet-facing<br/>HTTPS listener]
end
subgraph Internal["Internal Network"]
R2[Relay 2<br/>Post-pivot<br/>SMB listener]
end
subgraph DMZ
R3[Relay 3<br/>DNS C2 listener]
end
S <-->|WebSocket| R1
S <-->|WebSocket| R2
S <-->|WebSocket| R3
I1[Implant A] -->|HTTPS| R1
I2[Implant B] -->|SMB pipe| R2
I3[Implant C] -->|DNS| R3 Key Concepts¶
-
Each listener is bound to a specific relay via the
relay_idfield in the listener configuration. When you create a listener, you specify which relay should host it. -
Beacons check in to whichever relay hosts their listener. A beacon deployed via an HTTPS listener on Relay 1 will always communicate through Relay 1.
-
SOCKS tunnels can use any connected relay for traffic routing. The RelayHub selects the first available relay for tunnel frame delivery.
-
Multiple relays can serve different network segments. Use separate relays for:
- Internet-facing infrastructure (HTTPS listeners)
- Internal network pivots (SMB pipe listeners)
- Covert channels (DNS C2)
- Geographic distribution across different regions
Deployment Patterns¶
| Pattern | Relays | Use Case |
|---|---|---|
| Single relay | 1 | Lab testing, simple engagements |
| Dual relay | 2 | External + internal network access |
| Multi-segment | 3+ | Complex environments with network segmentation |
| Geographic | 2+ | Distributed targets across multiple sites |
Listener Assignment¶
When creating a listener, specify the target relay:
# Create HTTPS listener on Relay 1
curl -s -X POST https://stentor.app/api/v1/listeners \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "External HTTPS",
"type": "https",
"host": "10.0.0.50",
"port": 8443,
"relay_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}'
# Create SMB listener on Relay 2 (internal)
curl -s -X POST https://stentor.app/api/v1/listeners \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Internal SMB",
"type": "smb_pipe",
"host": "192.168.1.50",
"port": 445,
"relay_id": "<internal-relay-uuid>"
}'
Updating a Relay¶
To deploy a new version of the relay binary:
- Build the new binary on your development machine.
- Transfer via the jump host pattern described above.
- Restart the service:
# On the relay host (or via SSH)
sudo systemctl restart stentor-relay
# Verify the new version
journalctl -u stentor-relay | grep "Version:"
The relay re-establishes its WebSocket connection automatically on restart. Active listeners are automatically restored from persisted state (see Listener State Persistence) -- no manual restart required.
Automatic listener recovery
Listeners are now persisted to disk and auto-restored on relay restart. You no longer need to manually restart listeners after updating the relay binary.