Initial commit
This commit is contained in:
550
skills/compliance/policy-opa/references/kubernetes-security.md
Normal file
550
skills/compliance/policy-opa/references/kubernetes-security.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# Kubernetes Security Policies
|
||||
|
||||
Comprehensive OPA policies for Kubernetes security best practices and admission control.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pod Security](#pod-security)
|
||||
- [RBAC Security](#rbac-security)
|
||||
- [Network Security](#network-security)
|
||||
- [Image Security](#image-security)
|
||||
- [Secret Management](#secret-management)
|
||||
|
||||
## Pod Security
|
||||
|
||||
### Privileged Containers
|
||||
|
||||
Deny privileged containers:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.privileged_containers
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
container.securityContext.privileged == true
|
||||
|
||||
msg := sprintf("Privileged container is not allowed: %v", [container.name])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.initContainers[_]
|
||||
container.securityContext.privileged == true
|
||||
|
||||
msg := sprintf("Privileged init container is not allowed: %v", [container.name])
|
||||
}
|
||||
```
|
||||
|
||||
### Run as Non-Root
|
||||
|
||||
Enforce containers run as non-root:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.non_root
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
not container.securityContext.runAsNonRoot
|
||||
|
||||
msg := sprintf("Container must run as non-root user: %v", [container.name])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
container.securityContext.runAsUser == 0
|
||||
|
||||
msg := sprintf("Container cannot run as UID 0 (root): %v", [container.name])
|
||||
}
|
||||
```
|
||||
|
||||
### Read-Only Root Filesystem
|
||||
|
||||
Require read-only root filesystem:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.readonly_root
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
not container.securityContext.readOnlyRootFilesystem
|
||||
|
||||
msg := sprintf("Container must use read-only root filesystem: %v", [container.name])
|
||||
}
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
|
||||
Restrict Linux capabilities:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.capabilities
|
||||
|
||||
# Denied capabilities
|
||||
denied_capabilities := [
|
||||
"CAP_SYS_ADMIN",
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_SYS_PTRACE",
|
||||
"CAP_SYS_MODULE",
|
||||
]
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
capability := container.securityContext.capabilities.add[_]
|
||||
denied_capabilities[_] == capability
|
||||
|
||||
msg := sprintf("Capability %v is not allowed for container: %v", [capability, container.name])
|
||||
}
|
||||
|
||||
# Require dropping ALL capabilities by default
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
not drops_all_capabilities(container)
|
||||
|
||||
msg := sprintf("Container must drop ALL capabilities: %v", [container.name])
|
||||
}
|
||||
|
||||
drops_all_capabilities(container) {
|
||||
container.securityContext.capabilities.drop[_] == "ALL"
|
||||
}
|
||||
```
|
||||
|
||||
### Host Namespaces
|
||||
|
||||
Prevent use of host namespaces:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.host_namespaces
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
input.request.object.spec.hostPID == true
|
||||
|
||||
msg := "Sharing the host PID namespace is not allowed"
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
input.request.object.spec.hostIPC == true
|
||||
|
||||
msg := "Sharing the host IPC namespace is not allowed"
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
input.request.object.spec.hostNetwork == true
|
||||
|
||||
msg := "Sharing the host network namespace is not allowed"
|
||||
}
|
||||
```
|
||||
|
||||
### Host Paths
|
||||
|
||||
Restrict hostPath volumes:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.host_path
|
||||
|
||||
# Allowed host paths (if any)
|
||||
allowed_host_paths := [
|
||||
"/var/log/pods", # Example: log collection
|
||||
]
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
volume := input.request.object.spec.volumes[_]
|
||||
volume.hostPath
|
||||
not is_allowed_host_path(volume.hostPath.path)
|
||||
|
||||
msg := sprintf("hostPath volume is not allowed: %v", [volume.hostPath.path])
|
||||
}
|
||||
|
||||
is_allowed_host_path(path) {
|
||||
allowed_host_paths[_] == path
|
||||
}
|
||||
```
|
||||
|
||||
### Security Context
|
||||
|
||||
Comprehensive pod security context validation:
|
||||
|
||||
```rego
|
||||
package kubernetes.admission.security_context
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
not input.request.object.spec.securityContext
|
||||
|
||||
msg := "Pod must define a security context"
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
pod_security := input.request.object.spec.securityContext
|
||||
not pod_security.runAsNonRoot
|
||||
|
||||
msg := "Pod security context must set runAsNonRoot: true"
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
pod_security := input.request.object.spec.securityContext
|
||||
not pod_security.seccompProfile
|
||||
|
||||
msg := "Pod must define a seccomp profile"
|
||||
}
|
||||
```
|
||||
|
||||
## RBAC Security
|
||||
|
||||
### Wildcard Permissions
|
||||
|
||||
Prevent wildcard RBAC permissions:
|
||||
|
||||
```rego
|
||||
package kubernetes.rbac.wildcards
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Role"
|
||||
rule := input.request.object.rules[_]
|
||||
rule.verbs[_] == "*"
|
||||
|
||||
msg := sprintf("Role contains wildcard verb permission in rule: %v", [rule])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Role"
|
||||
rule := input.request.object.rules[_]
|
||||
rule.resources[_] == "*"
|
||||
|
||||
msg := sprintf("Role contains wildcard resource permission in rule: %v", [rule])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "ClusterRole"
|
||||
rule := input.request.object.rules[_]
|
||||
rule.verbs[_] == "*"
|
||||
|
||||
msg := sprintf("ClusterRole contains wildcard verb permission in rule: %v", [rule])
|
||||
}
|
||||
```
|
||||
|
||||
### Cluster Admin
|
||||
|
||||
Restrict cluster-admin usage:
|
||||
|
||||
```rego
|
||||
package kubernetes.rbac.cluster_admin
|
||||
|
||||
# System accounts allowed to use cluster-admin
|
||||
allowed_system_accounts := [
|
||||
"system:kube-controller-manager",
|
||||
"system:kube-scheduler",
|
||||
]
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "ClusterRoleBinding"
|
||||
input.request.object.roleRef.name == "cluster-admin"
|
||||
subject := input.request.object.subjects[_]
|
||||
not is_allowed_system_account(subject)
|
||||
|
||||
msg := sprintf("cluster-admin binding not allowed for subject: %v", [subject.name])
|
||||
}
|
||||
|
||||
is_allowed_system_account(subject) {
|
||||
allowed_system_accounts[_] == subject.name
|
||||
}
|
||||
```
|
||||
|
||||
### Service Account Token Mounting
|
||||
|
||||
Control service account token auto-mounting:
|
||||
|
||||
```rego
|
||||
package kubernetes.rbac.service_account_tokens
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
input.request.object.spec.automountServiceAccountToken == true
|
||||
not requires_service_account(input.request.object)
|
||||
|
||||
msg := "Pod should not auto-mount service account token unless required"
|
||||
}
|
||||
|
||||
requires_service_account(pod) {
|
||||
pod.metadata.annotations["requires-service-account"] == "true"
|
||||
}
|
||||
```
|
||||
|
||||
## Network Security
|
||||
|
||||
### Network Policies Required
|
||||
|
||||
Require network policies for namespaces:
|
||||
|
||||
```rego
|
||||
package kubernetes.network.policies_required
|
||||
|
||||
# Check if namespace has network policies (requires admission controller data)
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Namespace"
|
||||
not has_network_policy_annotation(input.request.object)
|
||||
|
||||
msg := sprintf("Namespace must have network policy annotation: %v", [input.request.object.metadata.name])
|
||||
}
|
||||
|
||||
has_network_policy_annotation(namespace) {
|
||||
namespace.metadata.annotations["network-policy.enabled"] == "true"
|
||||
}
|
||||
```
|
||||
|
||||
### Deny Default Network Policy
|
||||
|
||||
Implement default-deny network policy:
|
||||
|
||||
```rego
|
||||
package kubernetes.network.default_deny
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "NetworkPolicy"
|
||||
not is_default_deny(input.request.object)
|
||||
input.request.object.metadata.labels["policy-type"] == "default"
|
||||
|
||||
msg := "Default network policy must be deny-all"
|
||||
}
|
||||
|
||||
is_default_deny(network_policy) {
|
||||
# Check for empty ingress rules (deny all ingress)
|
||||
not network_policy.spec.ingress
|
||||
# Check for ingress type
|
||||
network_policy.spec.policyTypes[_] == "Ingress"
|
||||
}
|
||||
```
|
||||
|
||||
### Service Type LoadBalancer
|
||||
|
||||
Restrict external LoadBalancer services:
|
||||
|
||||
```rego
|
||||
package kubernetes.network.loadbalancer
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Service"
|
||||
input.request.object.spec.type == "LoadBalancer"
|
||||
not is_approved_for_external_exposure(input.request.object)
|
||||
|
||||
msg := sprintf("LoadBalancer service requires approval annotation: %v", [input.request.object.metadata.name])
|
||||
}
|
||||
|
||||
is_approved_for_external_exposure(service) {
|
||||
service.metadata.annotations["external-exposure.approved"] == "true"
|
||||
}
|
||||
```
|
||||
|
||||
## Image Security
|
||||
|
||||
### Image Registry Whitelist
|
||||
|
||||
Allow only approved image registries:
|
||||
|
||||
```rego
|
||||
package kubernetes.images.registry_whitelist
|
||||
|
||||
approved_registries := [
|
||||
"gcr.io/my-company",
|
||||
"docker.io/my-company",
|
||||
"quay.io/my-company",
|
||||
]
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
not is_approved_registry(container.image)
|
||||
|
||||
msg := sprintf("Image from unapproved registry: %v", [container.image])
|
||||
}
|
||||
|
||||
is_approved_registry(image) {
|
||||
startswith(image, approved_registries[_])
|
||||
}
|
||||
```
|
||||
|
||||
### Image Tags
|
||||
|
||||
Prevent latest tag and require specific tags:
|
||||
|
||||
```rego
|
||||
package kubernetes.images.tags
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
endswith(container.image, ":latest")
|
||||
|
||||
msg := sprintf("Container uses 'latest' tag: %v", [container.name])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
not contains(container.image, ":")
|
||||
|
||||
msg := sprintf("Container image must specify a tag: %v", [container.name])
|
||||
}
|
||||
```
|
||||
|
||||
### Image Vulnerability Scanning
|
||||
|
||||
Require vulnerability scan results:
|
||||
|
||||
```rego
|
||||
package kubernetes.images.vulnerability_scanning
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
not has_scan_annotation(input.request.object)
|
||||
|
||||
msg := "Pod must have vulnerability scan results annotation"
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
scan_result := input.request.object.metadata.annotations["vulnerability-scan.result"]
|
||||
scan_result == "failed"
|
||||
|
||||
msg := "Pod image failed vulnerability scan"
|
||||
}
|
||||
|
||||
has_scan_annotation(pod) {
|
||||
pod.metadata.annotations["vulnerability-scan.result"]
|
||||
}
|
||||
```
|
||||
|
||||
## Secret Management
|
||||
|
||||
### Environment Variable Secrets
|
||||
|
||||
Prevent secrets in environment variables:
|
||||
|
||||
```rego
|
||||
package kubernetes.secrets.env_vars
|
||||
|
||||
sensitive_keywords := [
|
||||
"password",
|
||||
"token",
|
||||
"apikey",
|
||||
"secret",
|
||||
"credential",
|
||||
]
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
container := input.request.object.spec.containers[_]
|
||||
env := container.env[_]
|
||||
is_sensitive_name(env.name)
|
||||
env.value # Direct value, not from secret
|
||||
|
||||
msg := sprintf("Sensitive data in environment variable: %v in container %v", [env.name, container.name])
|
||||
}
|
||||
|
||||
is_sensitive_name(name) {
|
||||
lower_name := lower(name)
|
||||
contains(lower_name, sensitive_keywords[_])
|
||||
}
|
||||
```
|
||||
|
||||
### Secret Volume Permissions
|
||||
|
||||
Restrict secret volume mount permissions:
|
||||
|
||||
```rego
|
||||
package kubernetes.secrets.volume_permissions
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Pod"
|
||||
volume := input.request.object.spec.volumes[_]
|
||||
volume.secret
|
||||
volume_mount := input.request.object.spec.containers[_].volumeMounts[_]
|
||||
volume_mount.name == volume.name
|
||||
not volume_mount.readOnly
|
||||
|
||||
msg := sprintf("Secret volume mount must be read-only: %v", [volume.name])
|
||||
}
|
||||
```
|
||||
|
||||
### External Secrets
|
||||
|
||||
Require use of external secret management:
|
||||
|
||||
```rego
|
||||
package kubernetes.secrets.external
|
||||
|
||||
deny[msg] {
|
||||
input.request.kind.kind == "Secret"
|
||||
input.request.object.metadata.labels["environment"] == "production"
|
||||
not input.request.object.metadata.annotations["external-secret.enabled"] == "true"
|
||||
|
||||
msg := sprintf("Production secrets must use external secret management: %v", [input.request.object.metadata.name])
|
||||
}
|
||||
```
|
||||
|
||||
## Admission Control Integration
|
||||
|
||||
Example OPA Gatekeeper ConstraintTemplate:
|
||||
|
||||
```yaml
|
||||
apiVersion: templates.gatekeeper.sh/v1beta1
|
||||
kind: ConstraintTemplate
|
||||
metadata:
|
||||
name: k8spodsecsecurity
|
||||
spec:
|
||||
crd:
|
||||
spec:
|
||||
names:
|
||||
kind: K8sPodSecSecurity
|
||||
targets:
|
||||
- target: admission.k8s.gatekeeper.sh
|
||||
rego: |
|
||||
package k8spodsecurity
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
container := input.review.object.spec.containers[_]
|
||||
container.securityContext.privileged == true
|
||||
msg := sprintf("Privileged container not allowed: %v", [container.name])
|
||||
}
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
container := input.review.object.spec.containers[_]
|
||||
not container.securityContext.runAsNonRoot
|
||||
msg := sprintf("Container must run as non-root: %v", [container.name])
|
||||
}
|
||||
```
|
||||
|
||||
Example Constraint:
|
||||
|
||||
```yaml
|
||||
apiVersion: constraints.gatekeeper.sh/v1beta1
|
||||
kind: K8sPodSecSecurity
|
||||
metadata:
|
||||
name: pod-security-policy
|
||||
spec:
|
||||
match:
|
||||
kinds:
|
||||
- apiGroups: [""]
|
||||
kinds: ["Pod"]
|
||||
namespaces:
|
||||
- "production"
|
||||
- "staging"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
|
||||
- [OPA Gatekeeper Library](https://github.com/open-policy-agent/gatekeeper-library)
|
||||
- [NSA Kubernetes Hardening Guide](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/)
|
||||
- [CIS Kubernetes Benchmark](https://www.cisecurity.org/benchmark/kubernetes)
|
||||
Reference in New Issue
Block a user