From 76144da05b279ebe616fd21c995359a8624a66a7 Mon Sep 17 00:00:00 2001 From: Gleb Shvetsov Date: Fri, 12 Jun 2026 18:07:04 +0300 Subject: [PATCH 1/2] chore: add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..ffaf92959 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +## Goal + +This PR delivers the required lab submission and supporting workflow artifacts. + +## Changes + +- Added or updated `submissions/labN.md` +- Added or updated lab-related configuration files +- Documented testing commands, observed outputs, and screenshots where useful + +## Testing + +Commands used: + +```bash + +``` + +Observed output: + +```text + +``` + +## Artifacts & Screenshots + +- Submission file: `submissions/labN.md` +- Screenshots or evidence: `` + +## Checklist + +- [ ] Title is clear (`feat(labN): ` style) +- [ ] No secrets/large temp files committed +- [ ] Submission file at `submissions/labN.md` exists From 67b7dab1e3bd6f1447d2531efae7e6f4601ea77d Mon Sep 17 00:00:00 2001 From: Gleb Shvetsov Date: Thu, 9 Jul 2026 13:29:28 +0300 Subject: [PATCH 2/2] feat(lab9): falco custom rules and conftest policies --- .gitignore | 1 + labs/lab9/falco/rules/custom-rules.yaml | 33 ++++ labs/lab9/policies/extra/hardening.rego | 47 ++++++ submissions/lab9.md | 193 ++++++++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 labs/lab9/falco/rules/custom-rules.yaml create mode 100644 labs/lab9/policies/extra/hardening.rego create mode 100644 submissions/lab9.md diff --git a/.gitignore b/.gitignore index c6b279fad..78cfd808d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ node_modules/ .venv/ .DS_Store *.swp +labs/lab9/falco/logs/ diff --git a/labs/lab9/falco/rules/custom-rules.yaml b/labs/lab9/falco/rules/custom-rules.yaml new file mode 100644 index 000000000..ee6947178 --- /dev/null +++ b/labs/lab9/falco/rules/custom-rules.yaml @@ -0,0 +1,33 @@ +- rule: Write to /tmp by container + desc: Detect write operations to /tmp inside containers + condition: > + open_write + and container.id != host + and fd.name startswith /tmp/ + output: > + Container wrote to /tmp + (container=%container.name user=%user.name file=%fd.name command=%proc.cmdline) + priority: WARNING + tags: [container, drift] + +- list: mining_pool_ports + items: [3333, 4444, 5555, 7777, 14444, 19999, 45700] + +- list: miner_process_names + items: [xmrig, ethminer, cgminer, t-rex, claymore, nc] + +- rule: Possible Cryptominer Activity + desc: Detect container network connections or processes matching common cryptominer indicators + condition: > + container.id != host + and evt.type = connect + and ( + fd.sport in (mining_pool_ports) + or proc.name in (miner_process_names) + or fd.cip.name contains "minexmr" + ) + output: > + Possible cryptominer activity detected + (container=%container.name process=%proc.name command=%proc.cmdline target=%fd.sip:%fd.sport target_name=%fd.cip.name user=%user.name) + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] diff --git a/labs/lab9/policies/extra/hardening.rego b/labs/lab9/policies/extra/hardening.rego new file mode 100644 index 000000000..47dcae2dd --- /dev/null +++ b/labs/lab9/policies/extra/hardening.rego @@ -0,0 +1,47 @@ +package main + +containers := input.spec.template.spec.containers + +pod_security_context := object.get(input.spec.template.spec, "securityContext", {}) + +container_run_as_non_root(container) if { + object.get(object.get(container, "securityContext", {}), "runAsNonRoot", false) == true +} + +pod_run_as_non_root if { + object.get(pod_security_context, "runAsNonRoot", false) == true +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + not pod_run_as_non_root + not container_run_as_non_root(container) + + msg := sprintf("container %q must set runAsNonRoot=true at pod or container securityContext", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + object.get(object.get(container, "securityContext", {}), "allowPrivilegeEscalation", true) != false + + msg := sprintf("container %q must set allowPrivilegeEscalation=false", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + drops := object.get(object.get(object.get(container, "securityContext", {}), "capabilities", {}), "drop", []) + not "ALL" in drops + + msg := sprintf("container %q must drop ALL Linux capabilities", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + not object.get(object.get(object.get(container, "resources", {}), "limits", {}), "memory", false) + + msg := sprintf("container %q must set resources.limits.memory", [container.name]) +} diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..cc99a0c77 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,193 @@ +# Lab 9 — Submission + +## Task 1: Runtime Detection with Falco + +### Baseline alert A — Terminal shell in container + +JSON alert from Falco logs: + +```json +{"hostname":"ace44f3d3b8d","output":"2026-07-09T10:10:38.674429137+0000: Notice A shell was spawned in a container with an attached terminal | evt_type=execve user=root user_uid=0 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=containerd-shim command=sh -lc echo \"shell-in-container test\" terminal=34816 exe_flags=EXE_WRITABLE|EXE_LOWER_LAYER container_id=09a8125e72dc container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"09a8125e72dc","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.arg.flags":"EXE_WRITABLE|EXE_LOWER_LAYER","evt.time.iso8601":1783591838674429137,"evt.type":"execve","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo \"shell-in-container test\"","proc.exepath":"/bin/busybox","proc.name":"sh","proc.pname":"containerd-shim","proc.tty":34816,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Notice","rule":"Terminal shell in container","source":"syscall","tags":["T1059","container","maturity_stable","mitre_execution","shell"],"time":"2026-07-09T10:10:38.674429137Z"} +``` + +### Baseline alert B — Read sensitive file untrusted (`cat /etc/shadow`) + +```json +{"hostname":"ace44f3d3b8d","output":"2026-07-09T10:11:06.035481730+0000: Warning Sensitive file opened for reading by non-trusted program | file=/etc/shadow gparent=systemd ggparent= gggparent= evt_type=openat user=root user_uid=0 user_loginuid=-1 process=cat proc_exepath=/bin/busybox parent=containerd-shim command=cat /etc/shadow terminal=0 container_id=09a8125e72dc container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"09a8125e72dc","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783591866035481730,"evt.type":"openat","fd.name":"/etc/shadow","k8s.ns.name":null,"k8s.pod.name":null,"proc.aname[2]":"systemd","proc.aname[3]":null,"proc.aname[4]":null,"proc.cmdline":"cat /etc/shadow","proc.exepath":"/bin/busybox","proc.name":"cat","proc.pname":"containerd-shim","proc.tty":0,"user.loginuid":-1,"user.name":"root","user.uid":0},"priority":"Warning","rule":"Read sensitive file untrusted","source":"syscall","tags":["T1555","container","filesystem","host","maturity_stable","mitre_credential_access"],"time":"2026-07-09T10:11:06.035481730Z"} +``` + +### Custom rule + +```yaml +- rule: Write to /tmp by container + desc: Detect write operations to /tmp inside containers + condition: > + open_write + and container.id != host + and fd.name startswith /tmp/ + output: > + Container wrote to /tmp + (container=%container.name user=%user.name file=%fd.name command=%proc.cmdline) + priority: WARNING + tags: [container, drift] + +- list: mining_pool_ports + items: [3333, 4444, 5555, 7777, 14444, 19999, 45700] + +- list: miner_process_names + items: [xmrig, ethminer, cgminer, t-rex, claymore, nc] + +- rule: Possible Cryptominer Activity + desc: Detect container network connections or processes matching common cryptominer indicators + condition: > + container.id != host + and evt.type = connect + and ( + fd.sport in (mining_pool_ports) + or proc.name in (miner_process_names) + or fd.cip.name contains "minexmr" + ) + output: > + Possible cryptominer activity detected + (container=%container.name process=%proc.name command=%proc.cmdline target=%fd.sip:%fd.sport target_name=%fd.cip.name user=%user.name) + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] +``` + +### Custom rule fired + +```json +{"hostname":"ace44f3d3b8d","output":"2026-07-09T10:11:34.573859848+0000: Warning Container wrote to /tmp (container=lab9-target user=root file=/tmp/my-write.txt command=sh -lc echo \"test\" > /tmp/my-write.txt) container_id=09a8125e72dc container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"09a8125e72dc","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783591894573859848,"fd.name":"/tmp/my-write.txt","k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"sh -lc echo \"test\" > /tmp/my-write.txt","user.name":"root"},"priority":"Warning","rule":"Write to /tmp by container","source":"syscall","tags":["container","drift"],"time":"2026-07-09T10:11:34.573859848Z"} +``` + +### Tuning consideration + +The custom write-to-/tmp rule can be noisy because many legitimate applications use `/tmp` for cache files, temporary uploads, sockets, or logs. I would tune it with an `exceptions:` block for known safe containers, users, paths, or processes instead of adding many hardcoded `and not proc.name=...` conditions. The `and not` pattern is still useful for a small number of obvious false positives, but exceptions are cleaner and easier to maintain when the allowlist grows. + +--- + +## Task 2: Conftest Policy-as-Code + +### My policy file + +```rego +package main + +containers := input.spec.template.spec.containers + +pod_security_context := object.get(input.spec.template.spec, "securityContext", {}) + +container_run_as_non_root(container) if { + object.get(object.get(container, "securityContext", {}), "runAsNonRoot", false) == true +} + +pod_run_as_non_root if { + object.get(pod_security_context, "runAsNonRoot", false) == true +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + not pod_run_as_non_root + not container_run_as_non_root(container) + + msg := sprintf("container %q must set runAsNonRoot=true at pod or container securityContext", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + object.get(object.get(container, "securityContext", {}), "allowPrivilegeEscalation", true) != false + + msg := sprintf("container %q must set allowPrivilegeEscalation=false", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + drops := object.get(object.get(object.get(container, "securityContext", {}), "capabilities", {}), "drop", []) + not "ALL" in drops + + msg := sprintf("container %q must drop ALL Linux capabilities", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := containers[_] + not object.get(object.get(object.get(container, "resources", {}), "limits", {}), "memory", false) + + msg := sprintf("container %q must set resources.limits.memory", [container.name]) +} +``` + +### Compliant manifest passes + +```text +8 tests, 8 passed, 0 warnings, 0 failures, 0 exceptions +``` + +### Non-compliant manifest fails + +```text +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - container "juice" must drop ALL Linux capabilities +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - container "juice" must set allowPrivilegeEscalation=false +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - container "juice" must set resources.limits.memory +FAIL - labs/lab9/manifests/k8s/juice-unhardened.yaml - main - container "juice" must set runAsNonRoot=true at pod or container securityContext + +8 tests, 4 passed, 0 warnings, 4 failures, 0 exceptions +``` + +### Compose policy generalizes + +```text +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions + +FAIL - /tmp/bad-compose.yml - compose.security - services must set an explicit non-root user +FAIL - /tmp/bad-compose.yml - compose.security - services must set read_only: true + +4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions +``` + +### Why CI-time vs admission-time + +CI-time policy checks catch insecure manifests early during pull request review, before they reach a cluster. Admission-time checks catch anything that bypasses CI or is applied manually with `kubectl apply`. Running both gives defense in depth: developers get fast feedback before merge, and the cluster still has a final security gate at deployment time. + +--- + +## Bonus: Cryptominer Detection Rule + +### Rule + +```yaml +- list: mining_pool_ports + items: [3333, 4444, 5555, 7777, 14444, 19999, 45700] + +- list: miner_process_names + items: [xmrig, ethminer, cgminer, t-rex, claymore, nc] + +- rule: Possible Cryptominer Activity + desc: Detect container network connections or processes matching common cryptominer indicators + condition: > + container.id != host + and evt.type = connect + and ( + fd.sport in (mining_pool_ports) + or proc.name in (miner_process_names) + or fd.cip.name contains "minexmr" + ) + output: > + Possible cryptominer activity detected + (container=%container.name process=%proc.name command=%proc.cmdline target=%fd.sip:%fd.sport target_name=%fd.cip.name user=%user.name) + priority: CRITICAL + tags: [container, mitre_execution, mitre_command_and_control] +``` + +### Triggered alert + +```json +{"hostname":"ace44f3d3b8d","output":"2026-07-09T10:11:55.561104722+0000: Critical Possible cryptominer activity detected (container=lab9-target process=nc command=nc -w 2 127.0.0.1 3333 target=: target_name= user=root) container_id=09a8125e72dc container_name=lab9-target container_image_repository=alpine container_image_tag=3.20 k8s_pod_name= k8s_ns_name=","output_fields":{"container.id":"09a8125e72dc","container.image.repository":"alpine","container.image.tag":"3.20","container.name":"lab9-target","evt.time.iso8601":1783591915561104722,"fd.cip.name":null,"fd.sip":null,"fd.sport":null,"k8s.ns.name":null,"k8s.pod.name":null,"proc.cmdline":"nc -w 2 127.0.0.1 3333","proc.name":"nc","user.name":"root"},"priority":"Critical","rule":"Possible Cryptominer Activity","source":"syscall","tags":["container","mitre_command_and_control","mitre_execution"],"time":"2026-07-09T10:11:55.561104722Z"} +``` + +### Reflection + +I used mining-pool destination ports and suspicious miner-like process names as indicators because they are common and easy to detect at runtime. This can miss false-negative cases where a miner is renamed and tunnels traffic over normal HTTPS port 443. I would combine this rule with the Lecture 9 SLA matrix by treating critical runtime alerts as high-priority incidents with fast triage, escalation, and follow-up hardening in CI/admission policies.