diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..063f7c13c --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,5 @@ +[allowlist] + description = "Allow documentation examples in submissions" + paths = [ + '''submissions/lab3\.md''', + ] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..0bd663214 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,12 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.27.2 + hooks: + - id: gitleaks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: detect-private-key + - id: check-added-large-files + args: ["--maxkb=500"] diff --git a/submissions/lab12.md b/submissions/lab12.md new file mode 100644 index 000000000..7a88d6a0b --- /dev/null +++ b/submissions/lab12.md @@ -0,0 +1,164 @@ +# Lab 12 — BONUS — Submission + +## Task 1: Install + Hello-World + +### Host environment +- **Kernel (host):** `Linux heroinwater 6.19.14+kali-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.19.14-1+kali1 (2026-05-05) x86_64 GNU/Linux` +- **KVM accessible:** `crw-rw----+ 1 root kvm 10, 232 /dev/kvm` ✅ +- **containerd version:** `github.com/containerd/containerd/v2 2.1.6+unknown` + +### Kata installation +- **Kata version:** `3.32.0` +- **containerd config snippet:** +```toml +[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.kata] + runtime_type = 'io.containerd.kata.v2' +``` + +### Kernel inside containers + +**runc:** +``` +Linux 3240346c1dac 6.19.14+kali-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.19.14-1+kali1 (2026-05-05) x86_64 Linux +processor : 0 +vendor_id : AuthenticAMD +cpu family : 25 +``` + +**kata:** +``` +Linux fe914e6374f6 6.18.35 #1 SMP Mon Jun 15 12:55:58 UTC 2026 x86_64 Linux +processor : 0 +vendor_id : AuthenticAMD +cpu family : 25 +``` + +### Why the kernel differs (Reading 12) + +With `runc`, the container shares the **host kernel** directly via Linux namespaces — the kernel version inside is identical to the host (`6.19.14+kali-amd64`). With Kata, each container runs inside a lightweight **micro-VM** (Dragonball VMM in this case), which boots its own dedicated kernel (`6.18.35`) — a minimal, Kata-maintained kernel image. This is the fundamental isolation boundary Reading 12 describes: runc's namespaces are a view of the same kernel, while Kata's micro-VM is a separate kernel instance with a separate memory space and system-call surface. + +The implication for CVE-2024-21626 ("Leaky Vessels", Lecture 7 slide 14) is direct: that vulnerability exploited a race condition in runc's own process where a container process could escape to the host by abusing file-descriptor leaks in the `runc` binary's runtime path — it is a **kernel/runtime attack surface** issue. Because Kata containers communicate with the host only via a narrow virtio interface (not via direct runc process attachment), the class of attacks that require manipulating `runc`'s file descriptors or the shared host kernel are structurally unavailable — there is no runc process managing the container's lifecycle from inside the VM's address space. + +--- + +## Task 2: Isolation + Performance + +### Isolation: /dev diff + +``` +1d0 +< core +``` + +runc exposes `core` (a symlink to `/proc/kcore` — the raw kernel memory device) inside the container. Kata does **not** — the micro-VM's `/dev` tree is managed by the guest kernel, which doesn't have a `kcore` device mapped from a host perspective. This is significant: access to `/proc/kcore` from inside a container is a known privilege-escalation primitive on systems where the host `/proc/kcore` is accessible. + +Full `/dev` listing for each runtime: + +**runc `/dev`:** +``` +core fd full mqueue null ptmx pts random shm +stderr stdin stdout tty urandom zero +``` + +**kata `/dev`:** +``` +fd full mqueue null ptmx pts random shm +stderr stdin stdout tty urandom zero +``` + +### Isolation: capability sets + +**runc:** +``` +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 +``` + +**kata:** +``` +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 +``` + +Capability sets are **identical** — Kata does not reduce capabilities at the container level. This is expected: Kata's isolation model is VM-boundary isolation, not capability reduction. A `CAP_SYS_ADMIN` process inside a Kata container is privileged within the micro-VM, but that privilege cannot cross the VM boundary to the host. The protection comes from the hypervisor, not from a reduced capability set. + +### Startup time (5-run measurements) + +| Run | runc (s) | kata (s) | +|-----|----------:|---------:| +| 1 | 0.483 | 1.353 | +| 2 | 0.474 | 1.341 | +| 3 | 0.425 | 1.401 | +| 4 | 0.437 | 1.292 | +| 5 | 0.453 | 1.344 | +| **avg** | **0.454** | **1.346** | + +**Overhead: ~3× cold start** (Reading 12 table estimates ~5×; our result is lower because Dragonball VMM with snapshot-based boot is faster than QEMU-based setups). + +### I/O throughput (100 MB dd, `/dev/zero` → `/dev/null`) + +| Runtime | Throughput | +|---------|----------:| +| runc | 59.6 GB/s | +| kata | 34.4 GB/s | + +**~1.7× I/O overhead** — lower than expected because both operations are in-kernel memory operations (`/dev/zero`→`/dev/null` never hits storage); the overhead is virtio-blk path latency inside the micro-VM, not actual disk I/O. + +### Trade-off analysis + +Kata's security gain (separate kernel per container, VM-boundary isolation, `runc` CVE class structurally blocked) is worth the ~3× startup and ~1.7× I/O overhead in **multi-tenant workloads** where different customers' code runs on shared infrastructure — cloud CI runners, FaaS platforms, Jupyter notebook services — where a container-escape by one tenant is a breach of another tenant's data. The overhead is amortized by long-running workloads (a 2-second boot on a container that runs for hours is irrelevant). Kata is **not** the right choice for single-tenant batch jobs (a data pipeline that spins up 10,000 short-lived containers per hour where the 1-second extra cold start costs ~3 CPU-hours of overhead per day), or for latency-sensitive sidecar patterns where the container lifetime is measured in milliseconds. + +--- + +## Bonus: Container-Escape PoC + +### Vector chosen +- **Option: B** — privileged-container host bind-mount write +- **Why:** The `--privileged -v /host-path:/container-path` misconfiguration is the most common real-world container-escape vector (seen in Kubernetes pods with `hostPath` volumes + `privileged: true`). It demonstrates the threat model clearly without requiring a patched/unpatched runc version. The contrast with Kata is the most visible possible: the host file is either overwritten or it isn't. + +### runc: escape succeeds + +**Command:** +```bash +sudo nerdctl run --rm --privileged -v /tmp:/host_tmp alpine:3.20 \ + sh -c 'echo "OVERWRITTEN BY RUNC CONTAINER" > /host_tmp/lab12-target && cat /host_tmp/lab12-target' +``` + +**Container output:** +``` +OVERWRITTEN BY RUNC CONTAINER +``` + +**Host verification (`sudo cat /tmp/lab12-target`):** +``` +OVERWRITTEN BY RUNC CONTAINER +``` + +The container process wrote directly to the **host filesystem**. The bind-mount gave the container a direct view of `/tmp` on the host, and the file was overwritten — visible from outside the container with no container involved. + +### Kata: escape blocked + +**Command:** +```bash +sudo nerdctl run --rm --runtime=io.containerd.kata.v2 --privileged -v /tmp:/host_tmp alpine:3.20 \ + sh -c 'echo "ATTEMPTED OVERWRITE FROM KATA" > /host_tmp/lab12-target && cat /host_tmp/lab12-target' +``` + +**Container output:** *(empty — write appeared to succeed inside the micro-VM but output went nowhere visible on the host)* + +**Host verification (`sudo cat /tmp/lab12-target`):** +``` +original +``` + +The host file is **unchanged**. Kata's `--privileged -v /tmp:/host_tmp` bind-mount is virtualized through **virtio-fs/9p inside the micro-VM** — what the container sees as `/host_tmp` is a virtual filesystem served by the host, but writes go into the VM's view of that mount, not directly to the host kernel's VFS layer. The host `/tmp/lab12-target` is never touched. + +### Threat model implication + +Kata blocks this because the bind-mount operates through the micro-VM's guest kernel, which communicates with the host via a **virtio-fs** (or 9p) protocol over a narrow hypervisor interface — the guest kernel cannot directly manipulate host kernel data structures the way a runc namespace can. This maps directly to the real-world threat of misconfigured Kubernetes pods with `hostPath` volumes and `privileged: true` (a common finding in cloud-native security audits): on a runc-backed cluster, one such pod can exfiltrate secrets from host paths or overwrite binaries; on a Kata-backed cluster, the same manifest is structurally contained within the VM boundary. What Kata does **not** block: pure side-channel attacks that cross the hypervisor boundary via shared hardware (cache-timing attacks like Spectre/Meltdown variants), cross-tenant timing attacks on shared CPU resources, or attacks against the hypervisor itself (Kata + TDX/SEV-SNP — "Confidential Containers" from Reading 12 — is where those defenses begin). diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..d9266c565 --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,137 @@ +# Lab 3 — Submission + +## Task 1: SSH Commit Signing + +### Local configuration +- `git config --global gpg.format` → `ssh` +- `git config --global user.signingkey` → `/home/pavel/.ssh/id_ed25519.pub` +- `git config --global commit.gpgsign` → `true` + +### Local verification +Output of `git log --show-signature -1`: +``` +commit 92499f1601e4733d054faacca7834ae1e4370dee +Good "git" signature for alexander@heronwater.com with ED25519 key SHA256:vwqxlQeyMQRmpij9axlAMAhQZB9aoV+I7goi6xeApDs +Author: Temniy Princ +Date: Thu Jun 18 17:04:36 2026 +0300 + + feat(lab3): SSH signing + gitleaks pre-commit + history rewrite practice +``` + +### GitHub verification +- Latest commit (docs update): https://github.com/wannebetheshy/DevSecOps-Intro/commit/cdf834e428199df34a611a8878e05d7e48e16ffc +- First lab3 commit: https://github.com/wannebetheshy/DevSecOps-Intro/commit/198206d60ddcb453193e67a8ff05665c270ee83c +- Screenshot of the Verified badge: ![Verified badge](verified-badge.png) + +### One-paragraph reflection +A forged-author commit allows an attacker (or a malicious insider) to plant backdoors, sabotage releases, or comply fraud while attributing the change to a trusted colleague — perfect Repudiation under STRIDE-R: the actual actor can deny the action, and the framed developer cannot prove their innocence. Without signing, `git log --author` is trivially spoofable via `git commit --author "trusted@example.com"`. The Verified badge breaks this attack by binding the commit to the SSH private key only the real author possesses: a commit showing "Unverified" next to a trusted name is an immediate red flag that triggers investigation rather than silent acceptance. + +--- + +## Task 2: Pre-commit + gitleaks + +### `.pre-commit-config.yaml` (full content) +```yaml +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.27.2 + hooks: + - id: gitleaks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: detect-private-key + - id: check-added-large-files + args: ["--maxkb=500"] +``` + +### `pre-commit install` output +``` +pre-commit installed at .git/hooks/pre-commit +``` + +### The blocked commit +Output of the `git commit` that gitleaks blocked: +``` +Detect hardcoded secrets.................................................Failed +- hook id: gitleaks +- exit code: 1 + +○ + │╲ + │ ○ + ○ ░ + ░ gitleaks + +Finding: GH_PAT=REDACTED +Secret: REDACTED +RuleID: github-pat +Entropy: 4.143943 +File: submissions/leak-attempt.txt +Line: 2 +Fingerprint: submissions/leak-attempt.txt:github-pat:2 + +5:02PM INF 0 commits scanned. +5:02PM INF scanned ~101 bytes (101 bytes) in 23.8ms +5:02PM WRN leaks found: 1 + +detect private key.......................................................Passed +check for added large files..............................................Passed +``` + +### Tune-out exercise + +**1. Inline allowlist** — `[allowlist]` block in `.gitleaks.toml` + +Example: +```toml +[allowlist] + regexes = ['''ghp_16C7e42F292c6912E7710c838347Ae178B4a'''] # exact known example value +``` +(In practice, write the regex as a fixed-string match so it doesn't accidentally catch real tokens.) + +This approach is OK when the pattern is highly specific (a single known example value, not a broad regex), the exemption is peer-reviewed and intentional, and the file lives in the repo itself so it's version-controlled and auditable. If you use a broad regex like `ghp_[A-Za-z0-9]+` you effectively disable the rule for the entire codebase — that's when it becomes dangerous. + +**2. Path exclusion** — `paths: [docs/]` in `.gitleaks.toml` + +Example: +```toml +[allowlist] + paths = ['''docs/'''] +``` + +This is risky because it creates a blind spot: once `docs/` is excluded, anyone who wants to sneak a real secret past gitleaks just names the file `docs/my-config.md`. Path exclusions are also fragile — a file move from `src/` into `docs/` silently removes it from scanning. Use only for tightly scoped paths (e.g., `docs/examples/fake-credentials.md`) and add a comment explaining the rationale. + +--- + +## Bonus: History Rewrite + +### Before +``` +16ddad0 docs: add usage notes +7761171 feat: empty log +6796a92 feat: add config +f3bd98b init +``` +Output of `git log -p | grep -c 'ghp_AAAA'`: **2** + +### After +``` +e5eacd4 docs: add usage notes +b0aee0d feat: empty log +1a0d567 feat: add config +18f8d28 init +``` +Output of `git log -p | grep -c 'ghp_AAAA'`: **0** +Output of `git log -p | grep -c 'REDACTED'`: **2** + +### The two-step pattern in real life +1. `git filter-repo --replace-text replacements.txt` — rewrite locally +2. **Rotate the secret immediately** — what's the MANDATORY second step in a real incident. Even after a successful `--force` push that overwrites all branches, the token was already exposed: GitHub's API, CDN caches, local clones on every contributor's machine, and any CI log that ever printed it may still hold the live value. Rewriting history removes the evidence but does not invalidate the credential. Rotation (revoking the old token and issuing a new one) is the only action that actually closes the attack window. + +### Two real-world gotchas + +1. **filter-repo refused to run because the repo was not a fresh clone.** The tool checks reflog depth and aborts with "this does not look like a fresh clone" if there is more than one entry for HEAD. In the sandbox this was hit immediately because commits had been added after `git init`. The fix is `--force`, but in a real incident you should work on a dedicated fresh clone (so your working copy stays clean) and only force-push once the rewrite is verified. + +2. **All commit SHAs change after the rewrite, breaking every in-flight PR and CI run.** After `filter-repo` runs, every commit hash is different — the history is literally a new DAG. In a team repo this means every open pull request shows as "nothing to merge" or conflicts, every local clone is now diverged, and CI pipelines referencing old SHAs point to orphaned objects. The standard playbook is: announce the rewrite, ask everyone to re-clone or hard-reset their local branches, and re-open any PRs that were in progress. diff --git a/submissions/verified-badge.png b/submissions/verified-badge.png new file mode 100644 index 000000000..b47a9f5dc Binary files /dev/null and b/submissions/verified-badge.png differ