Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[allowlist]
description = "Allow documentation examples in submissions"
paths = [
'''submissions/lab3\.md''',
]
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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"]
164 changes: 164 additions & 0 deletions submissions/lab12.md
Original file line number Diff line number Diff line change
@@ -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).
137 changes: 137 additions & 0 deletions submissions/lab3.md
Original file line number Diff line number Diff line change
@@ -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 <alexander@heronwater.com>
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.
Binary file added submissions/verified-badge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.