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"]
30 changes: 30 additions & 0 deletions labs/lab9/falco/rules/custom-rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
- rule: Write to /tmp by container
desc: Detects any write to /tmp inside a container — common indicator of dropper activity or staging
condition: >
open_write
and container
and fd.directory = /tmp
output: >
Write to /tmp detected in container
(user=%user.name container=%container.name image=%container.image.repository
file=%fd.name cmd=%proc.cmdline)
priority: WARNING
tags: [container, drift]

- rule: Possible Cryptominer Activity
desc: >
Detects a container connecting to well-known cryptomining pool ports or running
a known miner binary — maps to the Tesla 2018 K8s dashboard incident (Lecture 1)
condition: >
container
and (
(evt.type = connect and fd.rport in (3333, 4444, 5555, 7777, 14444, 19999, 45700))
or
(proc.name in (xmrig, ethminer, cgminer, t-rex, claymore, nbminer))
)
output: >
Possible cryptominer activity in container
(container=%container.name image=%container.image.repository
proc=%proc.name conn=%fd.name cmd=%proc.cmdline)
priority: CRITICAL
tags: [container, mitre_execution, mitre_command_and_control]
42 changes: 42 additions & 0 deletions labs/lab9/policies/extra/hardening.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

# 1. Pod or container must set runAsNonRoot: true
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.runAsNonRoot == true
not input.spec.template.spec.securityContext.runAsNonRoot == true
msg := sprintf("Deployment '%s': container '%s' must set runAsNonRoot: true", [input.metadata.name, c.name])
}

# 2. allowPrivilegeEscalation must be explicitly false on every container
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.allowPrivilegeEscalation == false
msg := sprintf("Deployment '%s': container '%s' must set allowPrivilegeEscalation: false", [input.metadata.name, c.name])
}

# 3. capabilities.drop must include ALL on every container
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not "ALL" in c.securityContext.capabilities.drop
msg := sprintf("Deployment '%s': container '%s' must drop ALL capabilities", [input.metadata.name, c.name])
}

# 4. Memory limits must be set (OOM kill bound)
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.resources.limits.memory
msg := sprintf("Deployment '%s': container '%s' must set resources.limits.memory", [input.metadata.name, c.name])
}

# 5. Image must not use :latest tag
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
endswith(c.image, ":latest")
msg := sprintf("Deployment '%s': container '%s' uses disallowed :latest tag — pin to a digest or explicit version", [input.metadata.name, c.name])
}
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.
Loading