ContainerSSH Kubernetes Backend: Hardened Pod-per-Session SSH Access
The Problem
ContainerSSH is an SSH server that, rather than granting shell access to the host it runs on, proxies each incoming connection into an ephemeral container. With the Kubernetes backend, that container is a Pod launched by the ContainerSSH service account at connection time and torn down at disconnection. From the user’s perspective, they typed ssh bastion.example.com and landed in an interactive shell. From the cluster’s perspective, a new Pod appeared in a dedicated namespace, attached to the SSH session, and will be deleted when the session ends.
That model offers genuine isolation: no two users share a process namespace, a writable filesystem, or a network identity. But isolation is not the same as security, and the gap between those two properties is exactly where this article lives.
The security of every session Pod is entirely determined by its spec, and the spec is returned dynamically by a config webhook that ContainerSSH calls before Pod creation. If that webhook returns a default spec — or if the operator never configures a hardened template — users receive Pods with allowPrivilegeEscalation: true, automounted service account tokens, no seccomp profile, and no resource limits. Any of those individually is a misconfiguration. Together, they hand an attacker who can reach the SSH listener a privileged foothold inside the cluster.
There are three compounding factors that make the ContainerSSH Kubernetes backend specifically risky:
The service account has create-and-exec permissions by default. ContainerSSH needs to create Pods, wait for them to be ready, and then attach to them via exec. An overly broad service account — particularly one granted cluster-admin for convenience, or one that has pods/* across all namespaces — means that code running inside a session Pod can call the Kubernetes API using the node or namespace service account token and perform operations the operator never intended to permit.
The config webhook response is the entire security boundary. ContainerSSH itself does not harden the Pod spec. It takes whatever the webhook returns and sends it to the Kubernetes API verbatim. If the webhook returns an empty spec, the Pod inherits defaults. If the webhook’s auth is misconfigured and an attacker can forge a webhook response, they control the spec of every session Pod created by the next SSH connection.
Session Pods can outlive their sessions. If ContainerSSH crashes mid-session or is forcibly killed, the session Pod may not be cleaned up. A privileged Pod that persists in the cluster after its session has ended is an orphaned attack surface with no active user monitoring it.
This article hardens all three surfaces: the service account and its RBAC, the Pod spec returned by the config webhook, and the lifecycle management that ensures session Pods do not persist.
Threat Model
Overly permissive ContainerSSH ServiceAccount. ContainerSSH runs with a Kubernetes ServiceAccount to create and manage session Pods. If that ServiceAccount is granted broad permissions — pods/* cluster-wide, or worse, cluster-admin — an attacker who escapes the session Pod container can use the mounted service account token to call the Kubernetes API directly. From a kubectl exec session in the session Pod, the attacker reads secrets across namespaces, lists all workloads, and can create new privileged Pods in any namespace where the service account has create permissions. The session Pod itself becomes a persistence mechanism.
Pod spec with allowPrivilegeEscalation: true. Without an explicit allowPrivilegeEscalation: false in the container’s securityContext, a container process with any file capability set (cap_setuid, cap_setgid) can execute a setuid binary and obtain root within the container. On a misconfigured node with a host path volume or a writable /proc, root in the container becomes a viable path to host escape. Kubernetes defaults are permissive: unless the namespace’s Pod Security Admission enforces restricted, allowPrivilegeEscalation defaults to true.
Missing NetworkPolicy. Session Pods running without a restricting NetworkPolicy can reach any cluster-internal endpoint: the Kubernetes API server, the etcd client port if it is exposed on the control plane subnet, the kubelet API on port 10250 on each node, other Pods in any namespace, and cloud metadata services at 169.254.169.254. An attacker who obtains a shell through a legitimately authenticated SSH session needs only a missing NetworkPolicy to pivot to cluster infrastructure.
Session Pod not deleted on crash. ContainerSSH tracks the lifecycle of session Pods and deletes them when sessions end. But ContainerSSH is itself a process that can crash. A hard OOM kill, a node eviction, or a deployment rollout that terminates ContainerSSH mid-session leaves behind Pods that will not be garbage collected by Kubernetes — Kubernetes only cleans up completed or failed Pods through its Pod garbage collection settings, and a running Pod that is simply no longer monitored stays running indefinitely. If that Pod was created with a non-hardened spec, it is now a privileged shell with no owner watching it.
Config webhook forgery or misconfiguration. The ContainerSSH config webhook is an HTTPS endpoint that ContainerSSH calls on each connection to obtain the session’s Pod spec. If the webhook endpoint is not mutually authenticated, an attacker who can intercept or forge responses can inject an arbitrary Pod spec — hostPID: true, hostNetwork: true, a hostPath volume mounting / — into the next SSH session’s Pod.
Hardening Configuration
1. Dedicated Namespace and Minimal RBAC
Create a dedicated namespace for all SSH session Pods. ContainerSSH itself runs in a separate namespace. The service account that ContainerSSH uses to manage session Pods needs only three permissions in the session namespace: create Pods, delete Pods, and exec into Pods.
# namespace for SSH session Pods
apiVersion: v1
kind: Namespace
metadata:
name: ssh-sessions
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.28
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
# ServiceAccount in the ContainerSSH namespace
apiVersion: v1
kind: ServiceAccount
metadata:
name: containerssh
namespace: containerssh
automountServiceAccountToken: false
# Role scoped to the ssh-sessions namespace only
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: containerssh-session-manager
namespace: ssh-sessions
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: containerssh-session-manager
namespace: ssh-sessions
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: containerssh-session-manager
subjects:
- kind: ServiceAccount
name: containerssh
namespace: containerssh
The Role is not a ClusterRole. ContainerSSH has no permissions outside the ssh-sessions namespace. If the ContainerSSH service account token is stolen and used to call the API, the attacker can list Pods in ssh-sessions — that is the ceiling.
2. ResourceQuota on the Session Namespace
Limit the maximum number of concurrent session Pods to prevent a single user (or a credential stuffing attack) from exhausting cluster resources:
apiVersion: v1
kind: ResourceQuota
metadata:
name: ssh-sessions-quota
namespace: ssh-sessions
spec:
hard:
pods: "50"
requests.cpu: "25"
requests.memory: "50Gi"
limits.cpu: "50"
limits.memory: "100Gi"
Tune the pod count to your maximum expected concurrent sessions plus a 20% headroom. When the quota is exhausted, ContainerSSH’s pods/create call returns a 403 Forbidden and the new SSH connection is rejected before a Pod is started.
3. Hardened ContainerSSH Configuration with Pod Spec
The following config.yaml for ContainerSSH 0.5+/0.6 configures the Kubernetes backend and embeds a hardened Pod spec template. This spec is the default; the config webhook can override it per-user, but the override should only loosen resource limits, not relax security context fields.
# /etc/containerssh/config.yaml
ssh:
hostkeys:
- /etc/containerssh/host_key
auth:
webhook:
url: https://auth-webhook.containerssh.svc.cluster.local/password
configserver:
url: https://config-webhook.containerssh.svc.cluster.local/config
backend: kubernetes
kubernetes:
connection:
host: https://kubernetes.default.svc
cacert: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
pod:
namespace: ssh-sessions
podSpec:
metadata:
labels:
app.kubernetes.io/managed-by: containerssh
containerssh.io/session: "true"
annotations:
seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
automountServiceAccountToken: false
enableServiceLinks: false
hostNetwork: false
hostPID: false
hostIPC: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
sysctls: []
containers:
- name: shell
image: ghcr.io/example/ssh-session-image:v1.2.3@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: home
mountPath: /home/user
volumes:
- name: tmp
emptyDir:
sizeLimit: "100Mi"
- name: home
emptyDir:
sizeLimit: "1Gi"
restartPolicy: Never
Key decisions in this spec:
automountServiceAccountToken: falseat the Pod spec level prevents the Kubernetes token from being mounted even if a container image’s default entrypoint expects it. An attacker in the session Pod has no token to call the Kubernetes API with.readOnlyRootFilesystem: truewith explicitemptyDirvolumes for/tmpand/home/usermeans the container filesystem is immutable. Tools that write to/etc,/usr, or other locations fail. This is intentional: session containers should not be modifying themselves.restartPolicy: Neverensures that if the container process exits (normal or abnormal), Kubernetes does not restart it. The session ends. There is no lingering container.- Image is pinned to a digest, not a tag. If the image registry is compromised and a new image is pushed to the same tag, existing sessions are not affected and new sessions use the pinned digest.
enableServiceLinks: falseprevents Kubernetes from injecting environment variables describing other services in the namespace into the session container — these variables leak internal service names and ports.
4. NetworkPolicy for Session Pods
The following NetworkPolicy denies all ingress to session Pods and restricts egress to a specific internal service (replace target-service with your actual destination):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ssh-session-isolation
namespace: ssh-sessions
spec:
podSelector:
matchLabels:
containerssh.io/session: "true"
policyTypes:
- Ingress
- Egress
ingress: []
egress:
# Allow DNS resolution via CoreDNS
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
# Allow egress to specific target service only
# Replace this block with your actual allowed destinations
- ports:
- protocol: TCP
port: 8443
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: target-namespace
podSelector:
matchLabels:
app: target-service
This policy has no ingress rules at all, which means no inbound traffic is permitted. ContainerSSH connects to the session Pod via pods/exec — that is an API server-mediated websocket, not a direct network connection to the Pod, so the NetworkPolicy does not block it.
Critically, this policy blocks egress to:
- The Kubernetes API server (no route to control plane subnet)
- The kubelet API on port 10250
- Cloud metadata services at 169.254.169.254
- Other Pods in all namespaces not explicitly allowed
- The internet
5. Pod Cleanup: CronJob for Orphaned Sessions
ContainerSSH deletes session Pods when sessions end, but orphaned Pods can accumulate if ContainerSSH is killed mid-session. The following CronJob garbage-collects any session Pod in the ssh-sessions namespace that has been running for more than one hour:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ssh-session-gc
namespace: ssh-sessions
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ssh-session-gc
namespace: ssh-sessions
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["list", "delete", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ssh-session-gc
namespace: ssh-sessions
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ssh-session-gc
subjects:
- kind: ServiceAccount
name: ssh-session-gc
namespace: ssh-sessions
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: ssh-session-gc
namespace: ssh-sessions
spec:
schedule: "*/15 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
serviceAccountName: ssh-session-gc
automountServiceAccountToken: true
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: gc
image: bitnami/kubectl:1.28@sha256:placeholder000000000000000000000000000000000000000000000000000000
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
command:
- /bin/sh
- -c
- |
CUTOFF=$(date -d '1 hour ago' --iso-8601=seconds)
kubectl get pods -n ssh-sessions \
-l containerssh.io/session=true \
--field-selector=status.phase=Running \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.creationTimestamp}{"\n"}{end}' \
| while IFS=$'\t' read -r name ts; do
if [ "$ts" \< "$CUTOFF" ]; then
echo "Deleting orphaned session pod: $name (created: $ts)"
kubectl delete pod "$name" -n ssh-sessions --grace-period=30
fi
done
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
The CronJob runs every 15 minutes. It uses containerssh.io/session=true as the selector, which is the label applied by the ContainerSSH Pod spec template above. The grace period of 30 seconds allows any active exec connections to drain before the Pod is forcibly terminated.
6. Kyverno Policy Enforcing Hardened Pod Spec
Even with a hardened config webhook, the ssh-sessions namespace should have a Kyverno ClusterPolicy that blocks any Pod in that namespace which does not meet the required security posture. This prevents a misconfigured webhook response from creating a non-hardened session Pod:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-ssh-session-pod-security
annotations:
policies.kyverno.io/title: Enforce SSH Session Pod Security
policies.kyverno.io/category: SSH Access Security
policies.kyverno.io/severity: high
policies.kyverno.io/description: >-
All Pods in the ssh-sessions namespace must have a hardened security
context. This policy blocks Pods that allow privilege escalation, mount
service account tokens, use host namespaces, or lack seccomp profiles.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-non-root
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pods must run as non-root (runAsNonRoot: true)"
pattern:
spec:
securityContext:
runAsNonRoot: true
- name: deny-privilege-escalation
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pod containers must set allowPrivilegeEscalation: false"
pattern:
spec:
containers:
- securityContext:
allowPrivilegeEscalation: false
- name: require-readonly-rootfs
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pod containers must have readOnlyRootFilesystem: true"
pattern:
spec:
containers:
- securityContext:
readOnlyRootFilesystem: true
- name: deny-automount-serviceaccount-token
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pods must set automountServiceAccountToken: false"
pattern:
spec:
automountServiceAccountToken: false
- name: deny-host-namespaces
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pods must not use host namespaces"
pattern:
spec:
=(hostPID): false
=(hostIPC): false
=(hostNetwork): false
- name: require-drop-all-capabilities
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pod containers must drop ALL capabilities"
pattern:
spec:
containers:
- securityContext:
capabilities:
drop:
- ALL
- name: require-resource-limits
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["ssh-sessions"]
validate:
message: "SSH session Pod containers must specify CPU and memory limits"
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
This policy uses validationFailureAction: Enforce, which means it blocks non-compliant Pod creation at admission time. If ContainerSSH’s config webhook returns a spec that fails any of these rules, the API server rejects the Pod and ContainerSSH returns an error to the SSH client — the session fails closed rather than open.
The background: true setting also runs these rules against existing Pods, flagging any that were created before the policy was applied or that slipped through during a Kyverno outage.
Expected Behaviour
The following table maps user actions to the Pod lifecycle and the corresponding audit events that should appear in your Kubernetes audit log.
| User Action | Pod Lifecycle | Audit Event |
|---|---|---|
| User connects via SSH | ContainerSSH calls config webhook; webhook returns hardened Pod spec; ContainerSSH calls POST /api/v1/namespaces/ssh-sessions/pods; Pod enters Pending then Running |
ResponseComplete for create pods by the containerssh ServiceAccount in ssh-sessions |
| User runs a command | ContainerSSH calls POST /api/v1/namespaces/ssh-sessions/pods/{name}/exec; API server upgrades to WebSocket; command runs in container |
ResponseComplete for create pods/exec by the containerssh ServiceAccount |
| User disconnects cleanly | ContainerSSH closes the exec WebSocket; calls DELETE /api/v1/namespaces/ssh-sessions/pods/{name} |
ResponseComplete for delete pods by the containerssh ServiceAccount |
| ContainerSSH crashes mid-session | Pod continues running in Running state; ContainerSSH does not call delete |
No delete event; Pod remains until CronJob GC runs (within 15 minutes) |
| Session Pod reaches 1h age | CronJob GC script identifies Pod by creation timestamp; calls kubectl delete pod with 30s grace period |
ResponseComplete for delete pods by the ssh-session-gc ServiceAccount |
| ResourceQuota exhausted (>50 Pods) | POST /api/v1/namespaces/ssh-sessions/pods returns 403 Forbidden; ContainerSSH rejects the SSH connection |
ResponseForbidden for create pods by the containerssh ServiceAccount; quota event in ssh-sessions |
| Kyverno blocks non-compliant Pod spec | Admission webhook rejects the Pod creation; API server returns 403 Forbidden to ContainerSSH |
Kyverno audit log entry with BLOCKED result for enforce-ssh-session-pod-security; SSH connection fails |
Trade-offs
The hardened configuration above makes deliberate choices that constrain what session Pods can do. Some of those constraints create friction.
| Decision | Security Benefit | Operational Trade-off |
|---|---|---|
Dedicated ssh-sessions namespace with scoped Role |
ContainerSSH service account has no cluster-wide permissions; token theft cannot reach other namespaces | Pods cannot access resources in other namespaces even if the session scenario requires it; separate namespace adds an administrative boundary to manage |
automountServiceAccountToken: false |
No Kubernetes API token is available inside session Pods; stolen credentials cannot call the cluster API | Users who need kubectl access from within a session (e.g., for a developer debugging namespace) cannot authenticate to the cluster; a separate, scoped token must be injected via a projected volume if this is genuinely required |
readOnlyRootFilesystem: true |
Container filesystem is immutable; attackers cannot write payloads, install tools, or modify binaries on disk | Tools that write to paths outside the declared emptyDir volumes fail; session images must be built with all required files pre-installed; pip install, apt-get, and similar runtime installs will not work |
| ResourceQuota capping Pod count at 50 | Prevents a credential-stuffing attack or a single user from exhausting cluster compute; limits blast radius of a session-level DoS | When the quota is reached, legitimate users are rejected with an opaque error; quota must be tuned based on peak concurrent session counts and increased proactively as usage grows |
restartPolicy: Never |
Session Pods do not restart on exit; no lingering containers after session end | A legitimate session that crashes due to an OOM or container fault is not recovered; the user must reconnect and start a new session rather than resuming |
| NetworkPolicy denying egress by default | Session Pods cannot reach Kubernetes API, kubelet, etcd, cloud metadata, or other Pods | Any new target service that session Pods need to reach requires an explicit NetworkPolicy egress rule; forgetting to add the rule breaks the session silently from the user’s perspective |
Failure Modes
The table below lists the most operationally significant failure modes, their symptoms, and how to detect and remediate each one.
| Failure Mode | Symptom | Detection | Remediation |
|---|---|---|---|
ContainerSSH ServiceAccount missing pods/exec permission |
SSH connections appear to succeed (Pod is created and starts) but the session hangs or immediately drops; no interactive shell is delivered | Kubernetes audit log shows ResponseForbidden for create pods/exec by the containerssh ServiceAccount; ContainerSSH logs show exec error |
Add pods/exec create verb to the Role in ssh-sessions; verify with kubectl auth can-i create pods/exec --as=system:serviceaccount:containerssh:containerssh -n ssh-sessions |
| ResourceQuota exhausted | New SSH connections are rejected; users receive a connection refused or an authentication error depending on ContainerSSH error handling | Kubernetes event in ssh-sessions namespace with reason: FailedCreate and exceeded quota; kubectl describe resourcequota ssh-sessions-quota -n ssh-sessions shows hard limits reached |
Increase the pods quota if usage growth is legitimate; investigate whether orphaned Pods are consuming quota (run GC CronJob manually); alert on kube_resourcequota_used / kube_resourcequota_hard > 0.8 |
| Kyverno blocks ContainerSSH Pod creation | Session Pods are rejected at admission; users receive errors; Kyverno admission webhook returns 403; ContainerSSH logs show admission webhook denied |
Kyverno policy report in ssh-sessions shows FAIL results; Kyverno controller logs show enforce-ssh-session-pod-security blocking the Pod |
Diagnose which rule is failing: the config webhook is returning a spec that does not meet the policy requirements; fix the webhook response, not the policy |
| Orphaned Pod not garbage collected | Session Pods remain in Running state after the SSH session has ended; quota is consumed by idle Pods; orphaned Pods represent unmonitored attack surfaces |
Alert on Pods in ssh-sessions with age >1h; check GC CronJob logs for failures; kubectl get pods -n ssh-sessions --sort-by=.metadata.creationTimestamp |
If CronJob is failing, check its service account permissions and logs; manually delete orphaned Pods with kubectl delete pods -l containerssh.io/session=true -n ssh-sessions; investigate why ContainerSSH is not cleaning up (crash loop? webhook timeout?) |
| NetworkPolicy too restrictive for session target | Session Pods cannot reach the intended internal service; commands that connect to the target fail with connection refused or timeout | Users report that their session tools cannot connect to expected endpoints; no network traffic in the allowed egress direction | Add a specific egress rule to ssh-session-isolation NetworkPolicy for the target service’s namespace, pod selector, and port; do not widen to 0.0.0.0/0 to fix the problem |
| Config webhook returns default/empty Pod spec | Session Pods are created with Kubernetes defaults: allowPrivilegeEscalation: true, automounted service account token, no seccomp profile |
Kyverno policy reports show FAIL for deny-privilege-escalation, require-non-root, etc. in ssh-sessions; if Kyverno is enforcing, Pods are blocked |
Fix the config webhook to return a complete hardened spec; treat any FAIL in the Kyverno background scan for ssh-sessions as a P1 incident |
Related Articles
- ContainerSSH as a Bastion Replacement — architecting ContainerSSH in front of internal systems as a replacement for traditional jump hosts
- Kyverno Controller Security — hardening the Kyverno deployment itself so that the policy engine enforcing session Pod security cannot be bypassed
- ContainerSSH Network Isolation — deeper treatment of network-level isolation for ContainerSSH sessions, including egress filtering and service mesh integration
- ContainerSSH Webhook Auth Hardening — mutual TLS, request signing, and replay protection for the ContainerSSH auth and config webhooks
- ContainerSSH Audit Logging — structured audit logging for SSH sessions, tying ContainerSSH session events to Kubernetes audit log entries for a unified forensic trail