Security Policy¶
Table of Contents¶
- Supported Versions
- Reporting a Vulnerability
- Threat Model
- Security Architecture
- Supply Chain Security
- Incident Response
- Hardening Checklist
- Security Scanning
- License
Supported Versions¶
| Version | Supported | Notes |
|---|---|---|
| 1.x.x | Yes | Current stable release |
Versioning restarted at 1.0.0 with the first public release. Tags published before it were removed rather than left to be found: in each of them the protection guarantee was either incomplete or actively broken, so none should be used.
We recommend always running the latest stable version.
Reporting a Vulnerability¶
We take security seriously. If you discover a security vulnerability in KernelSeal, please report it responsibly.
How to Report¶
- DO NOT create a public GitHub issue for security vulnerabilities
- Use GitHub's private vulnerability reporting: Report a vulnerability
- Or email: phonginreallife@gmail.com
Required Information¶
Please include:
| Field | Description |
|---|---|
| Summary | Brief description of the vulnerability |
| Severity | Your assessment (Critical/High/Medium/Low) |
| Affected Components | Which parts of KernelSeal are affected |
| Steps to Reproduce | Detailed reproduction steps |
| Impact | What an attacker could achieve |
| Proof of Concept | Code, logs, or screenshots if available |
| Suggested Fix | Optional remediation suggestions |
Response Timeline¶
| Phase | Timeline |
|---|---|
| Acknowledgment | Within 48 hours |
| Initial Assessment | Within 7 days |
| Status Update | Every 7 days until resolved |
Resolution by Severity:
| Severity | CVSS Score | Resolution Target |
|---|---|---|
| Critical | 9.0 - 10.0 | 7 days |
| High | 7.0 - 8.9 | 14 days |
| Medium | 4.0 - 6.9 | 30 days |
| Low | 0.1 - 3.9 | 60 days |
Scope¶
In Scope:
- KernelSeal agent binary and dependencies
- The
kernelseal-execshim and the socket protocol between them - BPF programs (
exec_monitor.bpf.c,lsm_file_protect.bpf.c) - Secret delivery and the protect-before-release ordering
- Policy enforcement bypass, including BPF/Go struct layout mismatches
- Container/VM escape via KernelSeal
- Privilege escalation
- Information disclosure
- Authentication/authorization bypass
Out of Scope:
- Vulnerabilities requiring physical access
- Social engineering attacks
- Denial of service without security impact
- Issues in unsupported versions
- Third-party dependencies without working exploit
Threat Model¶
Assets Protected¶
- Application Secrets - API keys, database credentials, tokens
- Process Memory - Runtime secret storage
- Kernel Integrity - BPF program execution environment
Threat Actors¶
| Actor | Capability | Motivation |
|---|---|---|
| Compromised Container | Root access within container | Steal secrets from other processes |
| Malicious Insider | Cluster access | Exfiltrate sensitive data |
| Supply Chain Attacker | Inject malicious code | Backdoor deployments |
| Adjacent Pod | Network access | Lateral movement |
Attack Vectors¶
┌─────────────────────────────────────────────────────────────────┐
│ Attack Surface │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ /proc/*/ │ │ ptrace() │ │ Environment │ │
│ │ environ,mem │ │ syscall │ │ Variables │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────┬─────────┴─────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ BPF-LSM Protection │ │
│ │ (Kernel-level Access Control) │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Mitigations by Attack Vector¶
| Attack Vector | Protection | Implementation |
|---|---|---|
/proc/*/environ read |
BPF-LSM file_open hook |
Blocks unauthorized readers |
/proc/*/mem read |
BPF-LSM file_open hook |
Blocks memory inspection |
/proc/*/maps read |
BPF-LSM file_open hook |
Optional, via blockMaps |
ptrace attach |
BPF-LSM ptrace_access_check |
Prevents debugger attach |
| Secrets on disk | Socket delivery | Nothing is written to a filesystem |
| Unprotected startup window | Protect-before-release ordering | See below |
| Enforcement unavailable | Fail closed | Secrets withheld in enforce mode |
| Container escape | Kernel verification | BPF verifier guarantees |
Secret Delivery and the Startup Window¶
Secrets are delivered by kernelseal-exec, which connects to the agent, receives
the environment, and then execves the target binary. The agent marks the
caller's PID protected before writing the response. Because execve preserves
the PID, the application starts already protected; there is no interval during
which a process holds secrets that can still be read out of /proc.
Two properties follow:
- The caller is identified with
SO_PEERCRED, which the kernel populates. A client cannot claim to be a different PID or UID. - The PID is paired with its start time from
/proc/<pid>/statand re-checked after protection is applied, so a caller that exits mid-handshake cannot cause protection to be attached to a recycled PID.
Authorization Boundary¶
The binary name in a request is a claim, not an identity. The agent cannot
verify which binary the shim is about to execute: at handshake time
/proc/<pid>/exe still points at the shim itself, because the execve has not
happened yet. The binary name therefore selects which secrets apply; it never
establishes who is asking.
Before 1.2.0 that claim was the only authorization input, so any process able to open the socket could request the secrets bound to any configured binary by naming it. This required no subterfuge: the request is a JSON object containing a string, so a compromised container did not need to install the binary, rename anything, or execute anything. Writing to the socket was sufficient.
# Any process that can reach the socket, without the shim involved at all
echo '{"binary":"/usr/bin/node"}' | nc -U /run/kernelseal/kernelseal.sock
With a node-wide hostPath socket, that meant any pod on the node that mounted
/run/kernelseal could obtain every secret in that node's configuration: self-only
on a dedicated node, cross-tenant secret disclosure on a shared one.
What authorizes a caller now¶
Since 1.2.0 the agent authorizes on the caller's cgroup, which the kernel sets
when the container is created and which a process cannot change from inside it. The
agent reads the peer's cgroup path from procfs, resolves the pod UID out of it, and
looks that UID up against the pods scheduled on its own node. Bindings then select
on namespace, labels, container and cgroupPath, and the binary name only
narrows what that pod is already entitled to.
The command above now fails on a node-wide agent: nc is in some pod's cgroup, and
that pod does not match the binding's selector, so the request is refused and
audited whatever it calls itself.
How strictly this is applied is set by policy.podIdentity, because it depends
entirely on who can reach the socket, which is a deployment question:
| Mode | Behavior | Use when |
|---|---|---|
required |
Refuses any caller it cannot attribute to a pod, and rejects bindings that name no pod. | A node-wide DaemonSet. Every pod on the node can open the socket, so the socket is not a boundary. |
preferred (default) |
Enforces pod selectors when present; still serves bindings that carry none. | A per-pod sidecar. The emptyDir socket is reachable only from inside one pod, so reaching it already proves which pod is asking. |
disabled |
Does not identify callers at all. | Only where the pre-1.2.0 behavior is needed deliberately. |
deploy/manifests/daemonset.yaml ships required and
deploy/kernelseal-sidecar.yaml ships preferred. In required mode the agent
refuses to start if it cannot watch pods, rather than running as a pod that looks
healthy while refusing every request.
An unrecognized podIdentity value is treated as required, so a typo in the
setting that governs authorization cannot quietly widen it.
What this does not close¶
- Within a single pod, the binary name is still only a claim. Any process in a pod can request any binding that pod is entitled to by naming its binary. Pod identity separates tenants, not processes inside one tenant.
- A binding with no pod selector is served to any caller under
preferred. That is correct only if the socket is genuinely pod-scoped. If you run a node-wide agent, userequired; nothing else in the configuration will catch the mistake for you. - Pod labels are mutable. Anyone who can patch a pod's labels, or create a pod
carrying them, can make it match a
labelsselector. Bind onnamespaceas well, and treat label-write access as equivalent to access to the secrets those labels select. - The pod cache can be stale. It is a list-watch against the API server; a
label change takes effect when the watch delivers it. A caller the agent cannot
attribute to a known pod is refused in
requiredmode rather than served.
Cgroup namespaces and cgroupPath¶
The kernel renders /proc/<pid>/cgroup relative to the reading process's cgroup
namespace. An agent that has its own namespace, which is the default for a
container on a cgroup v2 host, therefore sees other pods anchored to itself:
# Agent in its own cgroup namespace, reading another container's cgroup
0::/../docker-7f2a660030ce....scope
# Same read, agent in the host's cgroup namespace
0::/system.slice/docker-7f2a660030ce....scope
Pod attribution is unaffected, because the pod UID is parsed from a path segment
rather than from the path as a whole, so namespace, labels and container
selectors work either way. Only cgroupPath is affected, and it refuses rather
than comparing two paths anchored differently, since a coincidental match would
authorize the wrong cgroup.
The agent detects this at startup and logs it:
[IDENTITY] Agent cgroup: /
[WARN] This agent sees its own cgroup as "/", which means it has its own cgroup
[WARN] namespace. Callers' cgroup paths will be rendered relative to it, so
[WARN] cgroupPath selectors cannot match and will refuse.
Prefer namespace and labels. Reach for cgroupPath only when the agent runs in
the host's cgroup namespace, and confirm the startup line reports a real path
rather than /.
Scoping the socket is still worthwhile defense in depth:
- Mount the socket volume only into pods that should receive those secrets. Use a
pod-scoped
emptyDirrather than a node-widehostPathwhen a DaemonSet is not required. - Prefer one binding set per pod over a single node-wide configuration listing every application's secrets.
- Keep the socket mode at the default
0660and usefsGroupto share it, rather than widening it to0666.
Trust Boundary¶
Inside the trust boundary:
- The agent, which holds every resolved secret value in memory.
- The
kernelseal-execshim, which holds plaintext secrets between reading the socket and callingexecve. - The BPF programs and their maps.
Outside the trust boundary:
- The application itself. It receives its own secrets and nothing else.
- Every other process on the host, including root, which the LSM hooks refuse.
Agent Restarts End Protection Silently¶
Restarting the agent leaves every already-running protected process unprotected, permanently, with nothing logged.
Protection is installed once, during the shim's handshake, immediately before
execve. The protected PID set lives in a BPF map owned by the agent process, and
the LSM programs are attached for that process's lifetime. When the agent exits,
whether from an upgrade, a rollout restart, an OOM kill, an eviction or node
pressure, the map and the attachment go with it. The replacement agent starts with an empty map.
Applications that are already running never handshake again, because their execve
is long past. Their /proc/<pid>/environ becomes readable again and no audit event
is emitted, because from the new agent's point of view those processes were never
protected. Nothing in the logs marks the transition.
This affects the DaemonSet and sidecar deployments equally. A sidecar container
restart leaves the application container running, so the same gap opens; native
sidecars (Kubernetes 1.29+ initContainers with restartPolicy: Always) behave
the same way.
Until this is fixed, treat an agent restart as an event that requires restarting the workloads it protects:
kubectl rollout restart ds/kernelseal -n kernelseal-system
kubectl rollout restart deploy/<your-app> -n <your-namespace> # agent first, then app
- Set
updateStrategy: OnDeleteon the DaemonSet so agent restarts are deliberate rather than triggered by any manifest change. - Alert on
kernelseal_protected_pidsfalling below the number of protected workloads on that node. It is the only signal this has happened.
The fix is to pin the BPF links and the protected-PID map to bpffs so both survive the agent process, which removes the gap during the restart as well as after it. Tracked for 1.1.0.
Residual Risks¶
| Risk | Impact | Notes |
|---|---|---|
| Agent restart | Running protected processes silently lose protection for the rest of their lives | Nothing is pinned to bpffs yet; restart the workload after the agent |
| Same-pod process requests another binary's secrets | Reads secrets bound to any configured binary | Socket reachability is the boundary; scope volumes per pod |
| Child processes inherit the environment | Children hold secrets but are not themselves protected | Protection is per-PID |
| Secrets remain in the agent's heap | Values may persist until garbage collected | Go strings cannot be reliably zeroed |
| BPF-LSM unavailable | No enforcement possible | Enforce mode fails closed and reports not ready |
| Compromised agent or shim image | Full disclosure | Both are inside the trust boundary; verify image integrity |
Security Architecture¶
Defense in Depth¶
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1: Kubernetes Security │
│ - RBAC, Network Policies, Pod Security Standards │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: Container Security │
│ - Read-only filesystem, Dropped capabilities, Seccomp │
├─────────────────────────────────────────────────────────────────┤
│ Layer 3: KernelSeal Application │
│ - Policy enforcement, Binary filtering, Audit logging │
├─────────────────────────────────────────────────────────────────┤
│ Layer 4: BPF-LSM Kernel Protection │
│ - Mandatory access control, Syscall interception │
└─────────────────────────────────────────────────────────────────┘
BPF Program Security¶
KernelSeal's BPF programs run in kernel space with strict safety guarantees:
| Control | Description |
|---|---|
| Verifier Protection | All programs pass Linux kernel BPF verifier |
| Bounded Execution | No unbounded loops, guaranteed termination |
| Memory Safety | All memory accesses bounds-checked |
| Type Safety | BTF (BPF Type Format) ensures type correctness |
| Minimal Hooks | Only essential syscalls are intercepted |
| ABI Pinning | Struct layouts are asserted against the C header in CI |
Why the ABI is a security control¶
The policy the kernel enforces is written from user space into a BPF map as a raw struct. If the Go struct and the C struct disagree on field order, the map update still succeeds, because both are the same size; the kernel simply reads each setting from the wrong byte. The result is a policy that silently differs from the configured one, with no error anywhere.
This is not hypothetical. An earlier revision of
bpf/lsm_file_protect.bpf.c declared its own copy of the policy struct that was
missing two fields, which meant blockPtrace: true had no effect and the setting
that actually enabled ptrace blocking was blockMaps. Both sides now include
bpf/kernelseal_common.h, and make abi-check fails the build if the layouts
drift or if any BPF source redeclares a shared struct.
Secret lifecycle:
- At rest - held in the source (Kubernetes Secret, file, or the agent's own environment), never copied to a container filesystem.
- In transit - passed over a unix socket with mode
0660. No temporary file, no tmpfs mount, no/procwrite. - In use - present in the target process's environment, with reads of
/proc/<pid>/environ,/proc/<pid>/memandptracerefused by BPF-LSM. - Disposal - the kernel reclaims the process's memory on exit, and the
sched_process_exittracepoint plus a periodic reconciler remove the PID from the protected set so a recycled PID does not inherit protection.
RBAC Configuration¶
Under policy.podIdentity: preferred or required the agent also needs to read
pods, because that is how a caller's cgroup becomes a namespace and a set of
labels. The watch is restricted to the agent's own node with a
spec.nodeName field selector, so the permission is cluster-scoped but the traffic
is not: the agent only ever sees pods it could actually be asked about.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kernelseal-pods
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"] # Caller attribution only
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kernelseal-role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"] # Read-only secret access
resourceNames: ["app-secrets"] # Specific secrets only
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kernelseal-binding
subjects:
- kind: ServiceAccount
name: kernelseal-sa
roleRef:
kind: Role
name: kernelseal-role
apiGroup: rbac.authorization.k8s.io
Network Policy¶
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: kernelseal-network
spec:
podSelector:
matchLabels:
app: kernelseal
policyTypes:
- Ingress
- Egress
ingress: [] # No inbound traffic
egress:
- to:
- namespaceSelector:
matchLabels:
name: vault # Only Vault namespace
ports:
- port: 8200
protocol: TCP
Production Policy Configuration¶
version: v1
policy:
mode: enforce # Always enforce in production
blockEnviron: true # Block /proc/*/environ
blockMem: true # Block /proc/*/mem
blockMaps: true # Block /proc/*/maps
blockPtrace: true # Block ptrace attach
allowSelfRead: true # Allow process self-inspection
auditAll: true # Log all access attempts
kernelBinaryFilter: true # Efficient kernel-side filtering
podIdentity: required # Node-wide agent: attribute every
# caller to a pod before serving it
secrets:
- name: myapp-secrets
selector:
binary: "myapp" # Narrows which of this pod's
# bindings apply. Not an identity.
namespace: production # Authorizes: derived from the
labels: # caller's cgroup, which the kernel
app: myapp # sets and a container cannot forge
secretRefs:
- name: DB_PASSWORD
source:
# Written by a Vault agent sidecar into the KernelSeal container.
# Direct vaultRef is not yet implemented.
fileRef: /vault/secrets/db-password
With podIdentity: required, a binding whose selector names only a binary is
rejected at load and every request for it is refused, rather than being served to
whichever pod asks first. The refusal is logged with the binding name so the
misconfiguration is visible instead of silently costing you the protection.
Note that auditAll: true logs every allowed access to a protected process as
well as every denial. It is useful while establishing a baseline, but on a busy
node it is a significant volume of events; leave it off unless you are actively
investigating.
Supply Chain Security¶
Build Verification¶
| Check | Tool | CI Integration |
|---|---|---|
| Dependency vulnerabilities | govulncheck | Every PR |
| Container vulnerabilities | Trivy | Every build |
| Code security issues | gosec, CodeQL | Every PR |
| Secret detection | Gitleaks, TruffleHog | Every commit |
| Dockerfile best practices | Hadolint | Every PR |
Verifying a Release¶
Release images and artifacts are signed with cosign
keyless signing. There is no public key to distribute: the signature carries a
short-lived Sigstore certificate proving which GitHub workflow, in which
repository, at which tag produced the artifact. That is what you verify against,
so both flags below are required. cosign verify without them accepts a
signature from any identity, which is the most common way this check is run
and gets nothing out of it.
IMAGE=ghcr.io/phonginreallife/kernelseal
VERSION=v1.0.0
IDENTITY="https://github.com/phonginreallife/kernelseal/.github/workflows/release.yaml@refs/tags/${VERSION}"
cosign verify \
--certificate-identity "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"${IMAGE}:${VERSION}"
Signatures are attached to the image digest rather than the tag, because a tag can be moved to a different image afterwards while a digest cannot. To pin what you verified, resolve the digest and deploy that:
The release tarballs are covered by a signature over checksums.txt, since that
file contains their SHA-256 hashes:
cosign verify-blob \
--bundle checksums.txt.cosign.bundle \
--certificate-identity "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
sha256sum -c checksums.txt
SBOM¶
Every release publishes an SPDX SBOM, both as a release asset
(kernelseal-<version>-sbom.spdx.json) and as a cosign attestation on the
image, so a cluster that only knows the digest can still recover it:
cosign verify-attestation \
--type spdxjson \
--certificate-identity "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"${IMAGE}:${VERSION}" | jq -r '.payload' | base64 -d | jq '.predicate' > sbom.json
grype sbom:sbom.json
Dependency Policy¶
- All dependencies pinned to specific versions
- Dependabot enabled for automated updates
- Security updates applied within 7 days
- Major version updates reviewed manually
Incident Response¶
Detection¶
KernelSeal provides audit logs for security monitoring:
# Blocked access attempts
kubectl logs -l app=kernelseal | grep "\[LSM BLOCKED\]"
# Accesses observed but allowed (audit mode, or auditAll)
kubectl logs -l app=kernelseal | grep "\[LSM AUDIT\]"
# Refused secret requests, e.g. protection unavailable or a recycled PID
kubectl logs -l app=kernelseal | grep "\[DENY\]"
# A pod asking for another pod's secrets. Each line carries the calling pod's
# namespace, name and UID, so it names the workload rather than a PID that is
# already gone by the time anyone reads the log.
kubectl logs -l app=kernelseal | grep "no binding admits this caller"
# Bindings the policy will not serve as written, logged once at config load.
# These are misconfigurations, not attacks, but they fail closed either way.
kubectl logs -l app=kernelseal | grep "REJECTED"
# Current counters
kubectl exec -it <kernelseal-pod> -- \
wget -qO- localhost:9090/metrics | grep -E "kernelseal_(access|secrets|pods)"
A steady stream of no binding admits this caller from one pod is a workload
asking for secrets it is not entitled to. Treat a sustained rate as an intrusion
signal, and a single burst right after a deployment as a selector that no longer
matches the labels the workload actually carries.
Response Playbook¶
1. Suspected Secret Compromise
# Immediate: Rotate affected secrets
vault write -force secret/data/myapp/db password=$(openssl rand -base64 32)
# Investigate: Check audit logs
kubectl logs -l app=kernelseal --since=1h | grep -E "(BLOCK|AUDIT)"
# Verify: the application received the rotated values. Note that exec'ing into
# the pod starts a new process, which is not the protected one, so its own
# environment will not contain the secrets.
kubectl logs -l app=kernelseal | grep "\[ISSUE\]" # names only, never values
2. Unauthorized Access Attempt
# Identify source
kubectl logs -l app=kernelseal | grep "PID=<suspicious_pid>"
# Check process details
kubectl exec -it <pod> -- cat /proc/<pid>/comm
kubectl exec -it <pod> -- cat /proc/<pid>/cmdline
# Escalate if needed
# - Container may be compromised
# - Consider pod termination and forensics
Alerting Integration¶
groups:
- name: kernelseal
rules:
# Something is repeatedly trying to read protected process state.
- alert: SecretAccessBlocked
expr: increase(kernelseal_access_blocked_total[5m]) > 10
for: 1m
labels:
severity: warning
annotations:
summary: "Multiple secret access attempts blocked"
description: "{{ $value }} access attempts blocked in the last 5 minutes"
# Enforcement is not actually active. In enforce mode this also means
# secret requests are being refused, so applications will fail to start.
- alert: KernelSealLSMNotLoaded
expr: kernelseal_lsm_loaded == 0
for: 5m
labels:
severity: critical
annotations:
summary: "KernelSeal is not enforcing protection"
description: "BPF-LSM is not loaded; check that the kernel was booted with bpf in its lsm= list"
# Applications are asking for secrets and not getting them.
- alert: KernelSealSecretsDenied
expr: increase(kernelseal_secrets_denied_total[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "KernelSeal refused a secret request"
description: "{{ $value }} requests refused in the last 5 minutes; check the agent logs for [DENY]"
Hardening Checklist¶
Pre-Deployment¶
- [ ] Confirm the kernel lists
bpfin/sys/kernel/security/lsm, whichdeploy/kernelseal-probe.yamlchecks per node - [ ] Verify the image signature with both
--certificate-identityand--certificate-oidc-issuer, and deploy the digest you verified - [ ] Review and customize policy configuration
- [ ] Configure the binary allowlist for your applications
- [ ] Set up secret source (Vault, K8s Secrets, etc.)
- [ ] Set
policy.podIdentity: requiredon any node-wide agent, and give every binding anamespaceorlabelsselector - [ ] Scope the socket volume to the pod that should receive those secrets
- [ ] Wrap each application entrypoint with
kernelseal-exec - [ ] Leave the shim's
-on-errorat itsfaildefault so an application cannot start unprotected - [ ] Configure RBAC with least privilege
- [ ] Set up network policies
- [ ] Configure alerting for security events
Runtime¶
- [ ] KernelSeal running in
enforcemode - [ ]
kernelseal_lsm_loadedreporting 1 - [ ]
kernelseal_pods_watchednon-zero on a node-wide agent, since a cold or empty pod cache means every request is refused - [ ] Readiness probe passing, which confirms enforcement is available
- [ ] Read-only root filesystem enabled on application containers
- [ ] Capabilities limited to
SYS_ADMIN,BPF,PERFMON,SYS_RESOURCE - [ ] Socket mode left at
0660with a sharedfsGroup - [ ] Resource limits configured
- [ ] Secrets rotated on schedule
- [ ]
updateStrategy: OnDeleteon the DaemonSet, so agent restarts are deliberate - [ ] A documented procedure to restart protected workloads after any agent restart, since protection does not survive it
Monitoring¶
- [ ] Audit logs forwarded to SIEM
- [ ] Alerts configured for blocked access
- [ ] Alert on
kernelseal_protected_pidsdropping below the expected count, which is the only indication that an agent restart ended protection - [ ] Regular review of access patterns
- [ ] Vulnerability scanning in CI/CD
- [ ] Dependency updates monitored
Periodic Review¶
- [ ] Quarterly: Review and update policies
- [ ] Monthly: Review audit logs for anomalies
- [ ] Weekly: Apply security updates
- [ ] On change: Re-validate security controls
Security Scanning¶
This repository includes automated security scanning:
| Tool | Purpose | Frequency |
|---|---|---|
| gosec | Go security static analysis | Every PR |
| govulncheck | Go vulnerability detection | Every PR |
| Trivy | Container vulnerability scanning | Every build |
| CodeQL | Semantic code analysis | Every PR |
| Gitleaks | Secret detection | Every commit |
| Hadolint | Dockerfile linting | Every PR |
| Dependabot | Dependency vulnerability alerts | Daily |
License¶
KernelSeal BPF programs are dual-licensed under GPL-2.0 OR BSD-3-Clause as required for BPF programs.
Userspace components are licensed under Apache-2.0.
Last updated: January 2026