From 356419b2b045ee652bde3cd96ed0d0a1b0981248 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Thu, 11 Jun 2026 23:14:25 +0400 Subject: [PATCH 01/14] docs(lab1,lab2): clarify GitHub auth vs signing SSH key roles; add publickey-denied pitfalls Signed-off-by: Dmitrii Creed --- labs/lab1.md | 20 ++++++++++++++++---- labs/lab2.md | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/labs/lab1.md b/labs/lab1.md index bb4e226d9..eb319e50f 100644 --- a/labs/lab1.md +++ b/labs/lab1.md @@ -91,9 +91,20 @@ git config --global commit.gpgsign true git config --global tag.gpgsign true ``` -Tell the platform your SSH key is a **signing key**: -- GitHub: Settings → SSH and GPG keys → **New SSH key**, key type **Signing Key** -- GitLab: Profile → SSH Keys → tick "Usage type: Authentication & signing" +Now register the key on the platform. GitHub treats **Authentication** and **Signing** as *separate* roles for the same key, so you add it under both: + +- **Authentication Key** — lets you `clone` / `fetch` / `push` over SSH (`git@github.com:…`). If you cloned over HTTPS, or have never seen `ssh -T git@github.com` greet you by name, you don't have one configured yet — add it now or the `upstream` SSH remote will fail in Lab 2. +- **Signing Key** — gives your commits the **Verified** badge. + +- 🐙 GitHub: Settings → SSH and GPG keys → **New SSH key** → add the **same** `~/.ssh/id_ed25519.pub` **twice**, once with Key type **Authentication Key** and once with **Signing Key**. +- 🦊 GitLab: Profile → SSH Keys → a single key with **Usage type: Authentication & signing** covers both. + +Confirm authentication works before moving on: + +```bash +ssh -T git@github.com +# expect: Hi YOUR_USERNAME! You've successfully authenticated... +``` ### 1.4: Make a Signed Commit @@ -303,7 +314,8 @@ In `submissions/lab1.md`: ## Common Pitfalls - 🪤 **PR template doesn't auto-populate** — make sure the template is on `main` *before* opening the PR -- 🪤 **Commits show "Unverified"** — the SSH key must be added as a *Signing Key* on GitHub (not just an authentication key) +- 🪤 **Commits show "Unverified"** — the key must also be added as a **Signing Key** on GitHub; an Authentication Key alone won't verify commits (they're separate roles — see §1.3) +- 🪤 **`git@github.com: Permission denied (publickey)` on clone/fetch/push** — the *reverse* gap: your key is registered for signing but not as an **Authentication Key**. Add it as Authentication too (§1.3) and confirm with `ssh -T git@github.com`. Quick unblock for the *public* upstream: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`git push` rejected on `main`** — that's the bonus rule working as designed; push to `feature/lab1` instead - 🪤 **`gpg.format=ssh` ignored** — confirm Git ≥ 2.34: `git --version` - 🪤 **Pushed to the wrong branch** — `git switch feature/lab1` before `git push` diff --git a/labs/lab2.md b/labs/lab2.md index fca7b3f22..aae9acfb3 100644 --- a/labs/lab2.md +++ b/labs/lab2.md @@ -223,6 +223,7 @@ git bisect reset ## Common Pitfalls +- 🪤 **`git@github.com: Permission denied (publickey)` on `git fetch upstream`** — *not* a remote-config bug (the error is at the SSH layer, before Git reads the repo). Your key isn't registered for **authentication** on GitHub — and a **Signing Key** (Lab 1) does *not* count for auth, they're separate roles. Add the same `~/.ssh/id_ed25519.pub` as an **Authentication Key** (Lab 1 §1.3), verify with `ssh -T git@github.com`, then re-run. To unblock right now, the public upstream fetches over HTTPS with no key: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`reset --hard` without committing first** — your *uncommitted* edits really *are* gone (reflog only saves committed work). Always check `git status` first - 🪤 **`tag -v` says "no signature"** — you used `git tag NAME` instead of `git tag -a -s NAME -m "..."` - 🪤 **Rebase conflicts** — resolve, then `git rebase --continue`. Never `git rebase --skip` unless you know what you're skipping From bfa345b2244ccfe8ae4d92caa24ed90f13fa6282 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Thu, 11 Jun 2026 23:31:10 +0400 Subject: [PATCH 02/14] =?UTF-8?q?docs(lab3):=20matrix=20renames=20required?= =?UTF-8?q?=20checks=20=E2=80=94=20warn=20+=20ci-ok=20gate=20pattern;=20se?= =?UTF-8?q?t=20honest=20cache=20expectations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 2.2: matrixed jobs report as 'test (1.23)' etc.; the old required 'test'/'vet' contexts hang at 'Expected' and block every student PR. Document both fixes (rename required checks / require a single ci-ok aggregation job with if: always()) - 2.4: QuickNotes has zero deps, so cache on/off won't move wall-clock; ask students to explain why + compare per-step timings instead - pitfalls for both Signed-off-by: Dmitrii Creed --- labs/lab3.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/labs/lab3.md b/labs/lab3.md index 9f0970b20..87344cfb3 100644 --- a/labs/lab3.md +++ b/labs/lab3.md @@ -160,6 +160,23 @@ Tips: - GitLab: `parallel:matrix:` - Set `fail-fast: false` (GH) or equivalent so a single bad cell doesn't cancel the others — you want to *see* which combo broke +> ⚠️ **The matrix renames your checks — update branch protection (1.6) or your PR blocks forever.** A matrixed `test` job reports as `test (1.23)` and `test (1.24)`; the old required check named `test` will sit at *"Expected — Waiting for status to be reported"* indefinitely, even though every real check is green. Two fixes: +> +> 1. **Quick:** in the branch-protection rule, replace `vet`/`test` with the matrixed names (`vet (1.23)`, `vet (1.24)`, `test (1.23)`, `test (1.24)`). +> 2. **Robust (recommended):** add one aggregation job and require *only* it — then the matrix can change freely without touching protection settings: +> +> ```yaml +> ci-ok: +> if: always() +> needs: [vet, test, lint] +> runs-on: ubuntu-24.04 +> steps: +> - run: | +> test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" +> ``` +> +> The `if: always()` matters — without it, a failed `needs` job *skips* `ci-ok`, and a skipped required check lets the PR through on some configurations. + ### 2.3: Skip docs-only changes Edit your trigger so the pipeline runs **only** when something in `app/` or your CI config itself changes. README edits should not burn 4 minutes of CI time. @@ -179,6 +196,8 @@ Capture wall-clock times from the CI UI for three scenarios: > 💡 To get a clean baseline, temporarily disable each optimization with a commit, take a screenshot of the run time, then restore. +> 🧪 **Expect the cache rows to be boring — that's the finding, not a failure.** QuickNotes has **zero third-party dependencies** (look at `app/go.mod` — no `require` block, no `go.sum`), so the module cache has nothing to store and total wall-clock barely moves with `cache: true` vs `cache: false`. Most of your 60–80 s is runner provisioning, checkout, and the Go toolchain download — none of which `setup-go`'s cache touches. Report what you measured and *explain why* (that's design question **f** in disguise). To see where caching *would* pay, compare the **per-step** durations (`setup-go`, `go test`) instead of job totals, and note which step a real dependency-heavy project would save on. + ### 2.5: Document In `submissions/lab3.md`: @@ -284,6 +303,8 @@ Answer in 4-6 sentences: - 🪤 **Forgot `working-directory` (or `cd app`) for Go commands** — Go modules live in `app/`, not the repo root; commands run from the root will fail with "no Go files" - 🪤 **`fail-fast: true` (the GH Actions default) in a matrix** — one fail cancels the others; you can't see *which* combo broke - 🪤 **Branch protection set on someone else's fork's `main`** — you can only protect *your* fork's `main`. The upstream course repo has its own protection +- 🪤 **PR stuck on "Expected — Waiting for status to be reported" after adding the matrix** — the matrix renamed `test` → `test (1.23)`/`test (1.24)`, but branch protection still requires the old `test` context, which will never report again. Update the required-check names or switch to the `ci-ok` aggregation job (see §2.2) +- 🪤 **"Caching didn't speed anything up"** — on a zero-dependency module that's the *correct* result, not a mistake (see §2.4); don't pad the timing table with numbers you didn't observe - 🪤 **`golangci-lint` version not pinned** — "latest" pulls a new release tomorrow that may flag your code with new rules. Pin `v2.5.0` exactly - 🪤 **GitLab CI: incorrect anchor syntax** (`<<: *name`) — GitLab is strict; use the in-platform CI Lint tool (`Project → CI/CD → Editor → Validate`) - 🪤 **Cache hits expire after 7 days of inactivity on GH** — that's expected; the cache key is what protects you against poisoning From 3161a57feff4402372e373cec9769d466d68a7e5 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 18:38:40 +0300 Subject: [PATCH 03/14] ci(lab3): add PR gate Signed-off-by: Maximilian Mifsud Bonici --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..7342ef3de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + vet: + name: vet + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: "1.24" + cache: false + + - name: Run go vet + working-directory: app + run: go vet ./... + + test: + name: test + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: "1.24" + cache: false + + - name: Run race tests + working-directory: app + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: "1.24" + cache: false + + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + working-directory: app + run: golangci-lint run \ No newline at end of file From 71b04ae6c8d153cbfb2a5249d74baa536573b8fc Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 18:44:43 +0300 Subject: [PATCH 04/14] test(lab3): demonstrate failing gate Signed-off-by: Maximilian Mifsud Bonici --- app/handlers_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/handlers_test.go b/app/handlers_test.go index 9dff2e3e5..ae58c19eb 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -1,3 +1,5 @@ +#EXTRA BREAKAGE + package main import ( From 544bfca45aa3eee03be089d0d8703ff7b6fbf2d8 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 18:47:08 +0300 Subject: [PATCH 05/14] Revert "test(lab3): demonstrate failing gate" This reverts commit 71b04ae6c8d153cbfb2a5249d74baa536573b8fc. Signed-off-by: Maximilian Mifsud Bonici --- app/handlers_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/handlers_test.go b/app/handlers_test.go index ae58c19eb..9dff2e3e5 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -1,5 +1,3 @@ -#EXTRA BREAKAGE - package main import ( From 7dbca6014832cc5ce99e35e369fa88b4050c8bbe Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:05:13 +0300 Subject: [PATCH 06/14] ci(lab3): add cache matrix and path filters Signed-off-by: Maximilian Mifsud Bonici --- .github/workflows/ci.yml | 52 +++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7342ef3de..72897c213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,17 +3,28 @@ name: ci on: push: branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" pull_request: branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" permissions: contents: read jobs: vet: - name: vet + name: vet (${{ matrix.go-version }}) runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -21,17 +32,25 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.24" - cache: false + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: | + app/go.mod + app/go.sum - name: Run go vet working-directory: app run: go vet ./... test: - name: test + name: test (${{ matrix.go-version }}) runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -39,8 +58,11 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.24" - cache: false + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: | + app/go.mod + app/go.sum - name: Run race tests working-directory: app @@ -58,11 +80,25 @@ jobs: uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.24" - cache: false + cache: true + cache-dependency-path: | + app/go.mod + app/go.sum - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 - name: Run golangci-lint working-directory: app - run: golangci-lint run \ No newline at end of file + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [vet, test, lint] + runs-on: ubuntu-24.04 + + steps: + - name: Check all required jobs passed + run: | + test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" \ No newline at end of file From ea579049797d838a6b1b3dbeaea2e5b078e3a58b Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:14:12 +0300 Subject: [PATCH 07/14] ci(lab3): measure cache timing Signed-off-by: Maximilian Mifsud Bonici --- .github/workflows/ci.yml | 49 +++++++--------------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72897c213..dd4ccb594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,28 +3,17 @@ name: ci on: push: branches: [main] - paths: - - "app/**" - - ".github/workflows/ci.yml" pull_request: branches: [main] - paths: - - "app/**" - - ".github/workflows/ci.yml" permissions: contents: read jobs: vet: - name: vet (${{ matrix.go-version }}) + name: vet runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - go-version: ["1.23", "1.24"] - steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -32,25 +21,18 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: ${{ matrix.go-version }} + go-version: "1.24" cache: true - cache-dependency-path: | - app/go.mod - app/go.sum + cache-dependency-path: app/go.mod - name: Run go vet working-directory: app run: go vet ./... test: - name: test (${{ matrix.go-version }}) + name: test runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - go-version: ["1.23", "1.24"] - steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -58,11 +40,9 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: ${{ matrix.go-version }} + go-version: "1.24" cache: true - cache-dependency-path: | - app/go.mod - app/go.sum + cache-dependency-path: app/go.mod - name: Run race tests working-directory: app @@ -81,24 +61,11 @@ jobs: with: go-version: "1.24" cache: true - cache-dependency-path: | - app/go.mod - app/go.sum + cache-dependency-path: app/go.mod - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 - name: Run golangci-lint working-directory: app - run: golangci-lint run - - ci-ok: - name: ci-ok - if: always() - needs: [vet, test, lint] - runs-on: ubuntu-24.04 - - steps: - - name: Check all required jobs passed - run: | - test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" \ No newline at end of file + run: golangci-lint run \ No newline at end of file From b7f21dd16455d9e7f946b9d3f9852fd04214bc89 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:18:18 +0300 Subject: [PATCH 08/14] ci(lab3): add matrix and path filters Signed-off-by: Maximilian Mifsud Bonici --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd4ccb594..65e5a0572 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,17 +3,28 @@ name: ci on: push: branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" pull_request: branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" permissions: contents: read jobs: vet: - name: vet + name: vet (${{ matrix.go-version }}) runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -21,7 +32,7 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.24" + go-version: ${{ matrix.go-version }} cache: true cache-dependency-path: app/go.mod @@ -30,9 +41,14 @@ jobs: run: go vet ./... test: - name: test + name: test (${{ matrix.go-version }}) runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go-version: ["1.23", "1.24"] + steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.24" + go-version: ${{ matrix.go-version }} cache: true cache-dependency-path: app/go.mod @@ -68,4 +84,15 @@ jobs: - name: Run golangci-lint working-directory: app - run: golangci-lint run \ No newline at end of file + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [vet, test, lint] + runs-on: ubuntu-24.04 + + steps: + - name: Check all required jobs passed + run: | + test "${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }}" = "true" \ No newline at end of file From 76cb6dfdd709f6ab2cb9f28d8368654e9816fbc2 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:29:14 +0300 Subject: [PATCH 09/14] docs(lab3): add submission Signed-off-by: Maximilian Mifsud Bonici --- submissions/lab3.md | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 submissions/lab3.md diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..d453d3b52 --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,82 @@ +# Lab 3 Submission + +Chosen path: GitHub Actions + +I chose GitHub Actions because the repository is hosted on GitHub and this is the default path for the lab. + +## Task 1 Evidence + +### Failed CI run + +I deliberately introduced a breaking change to prove that the PR gate blocks bad commits. The CI checks failed as expected. + +Failed run screenshot: https://gyazo.com/71cab95128f4fa3e7352b166059ba309 + +### Fixed CI run + +After confirming the gate failed correctly, I reverted/fixed the breaking change and pushed the correction. The CI checks passed again. + +Fixed / green run screenshot: https://gyazo.com/e17d7fcf32959171edd8bb25d9e53be2 + +### Branch protection + +Branch protection was enabled on `main` in my fork. The rule requires status checks to pass before merging, so the PR cannot be merged unless the CI pipeline is green. + +Branch protection screenshot: https://gyazo.com/6681bd00b8b3d6b65055b93f69626a57 + + +## Design Questions + +### a) Why pin the runner version instead of ubuntu-latest? + +Pinning `ubuntu-24.04` makes the CI environment predictable. `ubuntu-latest` can move to a newer Ubuntu image later, which may change installed packages, shell behavior, Go toolchain defaults, or dependency versions. That can break a previously green pipeline without any code change. + +### b) Why split vet, test, and lint into separate units? + +Splitting them gives clearer failure reports and allows the jobs to run in parallel. If everything is in one combined job, one early failure hides later failures, and the CI output becomes slower and less useful. + +### c) What real attack does SHA pinning prevent? + +SHA pinning protects against supply-chain attacks where an attacker compromises or retags a GitHub Action version tag. The lab references the `tj-actions/changed-files` supply-chain incident from March 2025. If a workflow used a mutable tag, it could suddenly execute malicious action code. A full commit SHA points to one exact immutable commit. + +### d) What is permissions: and what principle is behind it? + +`permissions:` controls what the GitHub Actions token is allowed to do inside the workflow. Setting `contents: read` follows least privilege: the workflow only gets the access it needs to read the repository, not broad write permissions it does not need. + +### e) GitLab path: what's the difference between a stage and a job? What would dependencies: do that stages: doesn't? + +A GitLab job is one unit of work, such as running tests or linting. A stage is a group/order level that controls when jobs run; for example, all jobs in the `test` stage can run before jobs in the `deploy` stage. `stages:` controls execution order, while `dependencies:` controls which previous job artifacts are downloaded by a job. So `dependencies:` is about artifact flow, not just job ordering. + +## Task 2: Make It Fast and Smart + +### Optimizations applied + +I enabled Go caching through `actions/setup-go` by setting `cache: true` and using `app/go.mod` as the cache dependency path. Since the Go module is inside `app/`, this makes the cache key depend on the actual module file used by the project. + +I added a Go version matrix for `vet` and `test`, running both jobs on Go `1.23` and Go `1.24`. This helps detect problems that only appear under one Go version. + +I added path filters so the workflow only runs when files inside `app/` or `.github/workflows/ci.yml` change. This prevents documentation-only changes from wasting CI minutes. + +I added a `ci-ok` aggregation job and made it the required branch-protection check. This avoids having to manually require every matrix check name such as `test (1.23)` and `test (1.24)`. + +### Timing table + +| Scenario | Wall-clock | +|----------|-----------:| +| Baseline: no cache, single Go version, no path filter | 1m 11s | +| With cache | 1m 5s | +| With cache + matrix | 1m 9s | + +Caching slightly reduced the total wall-clock time, but the improvement was small because QuickNotes has no third-party dependencies. Most of the workflow time is spent on runner startup, checkout, Go setup, and installing/running tools rather than downloading modules. The matrix version still completed in a similar time because the Go 1.23 and Go 1.24 jobs run in parallel, although it starts more jobs overall. + +### f) Why cache `go.sum`-keyed inputs and not build outputs? + +Caching should be based on deterministic inputs. In a Go project, `go.sum` records exact dependency versions and checksums, so it is a safe basis for deciding whether the module cache can be reused. Build outputs are less safe to cache blindly because they can depend on the operating system, CPU architecture, Go version, compiler flags, and environment. Reusing the wrong build outputs could hide real build problems or make the CI behave inconsistently. + +### g) What does `fail-fast: false` change in a matrix run, and when do you actually want `fail-fast: true`? + +`fail-fast: false` means that if one matrix job fails, GitHub Actions still continues running the other matrix jobs. This is useful here because I want to see whether a failure happens only on Go `1.23`, only on Go `1.24`, or on both versions. `fail-fast: true` is useful when jobs are expensive and one failure already proves the pipeline is unusable, because cancelling the remaining jobs saves time and CI minutes. + +### h) What is the risk of an attacker writing a cache from a malicious PR that protected branches later read? + +The risk is cache poisoning. A malicious pull request could try to store attacker-controlled files in a cache, and then a later trusted workflow might restore that cache and use those files. This is dangerous if the cached content affects the build, test, or release process. GitHub reduces this risk by limiting cache access depending on the branch and event context, but workflows should still avoid caching sensitive files or generated executables from untrusted code. \ No newline at end of file From f6b4e9130b0b413459700ad01f7afce6bc05a5e8 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:52:53 +0300 Subject: [PATCH 10/14] docs(lab4): add OS networking submission Signed-off-by: Maximilian Mifsud Bonici --- submissions/lab4-assets/curl-post.txt | 23 ++++++++++ submissions/lab4-assets/dig-example.txt | 3 ++ submissions/lab4-assets/go-run-processes.txt | 1 + submissions/lab4-assets/ip-route.txt | 3 ++ .../lab4-assets/journalctl-quicknotes.txt | 2 + submissions/lab4-assets/lab4-trace.pcap | Bin 0 -> 1240 bytes submissions/lab4-assets/lab4-trace.txt | 43 ++++++++++++++++++ submissions/lab4-assets/mtr-localhost.txt | 4 ++ .../lab4-assets/outsidein-1-process.txt | 2 + .../lab4-assets/outsidein-2-listening.txt | 2 + .../lab4-assets/outsidein-3-health.txt | 2 + .../lab4-assets/outsidein-4-firewall.txt | 1 + submissions/lab4-assets/outsidein-5-dns.txt | 2 + submissions/lab4-assets/qn-broken.log | 3 ++ submissions/lab4-assets/repair-health.txt | 1 + submissions/lab4-assets/ss-8080.txt | 2 + submissions/lab4.md | 0 17 files changed, 94 insertions(+) create mode 100644 submissions/lab4-assets/curl-post.txt create mode 100644 submissions/lab4-assets/dig-example.txt create mode 100644 submissions/lab4-assets/go-run-processes.txt create mode 100644 submissions/lab4-assets/ip-route.txt create mode 100644 submissions/lab4-assets/journalctl-quicknotes.txt create mode 100644 submissions/lab4-assets/lab4-trace.pcap create mode 100644 submissions/lab4-assets/lab4-trace.txt create mode 100644 submissions/lab4-assets/mtr-localhost.txt create mode 100644 submissions/lab4-assets/outsidein-1-process.txt create mode 100644 submissions/lab4-assets/outsidein-2-listening.txt create mode 100644 submissions/lab4-assets/outsidein-3-health.txt create mode 100644 submissions/lab4-assets/outsidein-4-firewall.txt create mode 100644 submissions/lab4-assets/outsidein-5-dns.txt create mode 100644 submissions/lab4-assets/qn-broken.log create mode 100644 submissions/lab4-assets/repair-health.txt create mode 100644 submissions/lab4-assets/ss-8080.txt create mode 100644 submissions/lab4.md diff --git a/submissions/lab4-assets/curl-post.txt b/submissions/lab4-assets/curl-post.txt new file mode 100644 index 000000000..6adc7850f --- /dev/null +++ b/submissions/lab4-assets/curl-post.txt @@ -0,0 +1,23 @@ +Note: Unnecessary use of -X or --request, POST is already inferred. +* Trying 127.0.0.1:8080... + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to localhost (127.0.0.1) port 8080 (#0) +> POST /notes HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.82.0 +> Accept: */* +> Content-Type: application/json +> Content-Length: 39 +> +} [39 bytes data] +* Mark bundle as not supporting multiuse +< HTTP/1.1 201 Created +< Content-Type: application/json +< Date: Tue, 16 Jun 2026 16:46:12 GMT +< Content-Length: 92 +< +{ [92 bytes data] + 100 131 100 92 100 39 15219 6451 --:--:-- --:--:-- --:--:-- 26200 +* Connection #0 to host localhost left intact +{"id":6,"title":"trace me","body":"in flight","created_at":"2026-06-16T16:46:12.62574443Z"} diff --git a/submissions/lab4-assets/dig-example.txt b/submissions/lab4-assets/dig-example.txt new file mode 100644 index 000000000..ee09d40a4 --- /dev/null +++ b/submissions/lab4-assets/dig-example.txt @@ -0,0 +1,3 @@ +$ dig +short example.com @1.1.1.1 +104.20.23.154 +172.66.147.243 diff --git a/submissions/lab4-assets/go-run-processes.txt b/submissions/lab4-assets/go-run-processes.txt new file mode 100644 index 000000000..ba677b0ac --- /dev/null +++ b/submissions/lab4-assets/go-run-processes.txt @@ -0,0 +1 @@ +root 4460 298 12 19:48 pts/0 00:00:00 /snap/go/11200/bin/go run . diff --git a/submissions/lab4-assets/ip-route.txt b/submissions/lab4-assets/ip-route.txt new file mode 100644 index 000000000..4733c94c9 --- /dev/null +++ b/submissions/lab4-assets/ip-route.txt @@ -0,0 +1,3 @@ +$ ip route show +default via 172.19.128.1 dev eth0 proto kernel +172.19.128.0/20 dev eth0 proto kernel scope link src 172.19.130.184 diff --git a/submissions/lab4-assets/journalctl-quicknotes.txt b/submissions/lab4-assets/journalctl-quicknotes.txt new file mode 100644 index 000000000..b0601f6da --- /dev/null +++ b/submissions/lab4-assets/journalctl-quicknotes.txt @@ -0,0 +1,2 @@ +$ journalctl --user -u quicknotes -n 20 || true +-- No entries -- diff --git a/submissions/lab4-assets/lab4-trace.pcap b/submissions/lab4-assets/lab4-trace.pcap new file mode 100644 index 0000000000000000000000000000000000000000..dfe48d8ee6a476e351241e1c34467e10d0b26fa0 GIT binary patch literal 1240 zcma)*PiWIn7{Fh;qHUl!y{M->a+1-1No&@Hf~{2?Q?XzK=S5=rY&&97()2K-t_r*8 zW#GX=3%24w#gj*`f;)H;2EB^b%k<(wkR95+FKyPYbiofkczN&T_rCYbmz_@^+8*RZ zqwpezKI~Na)!$G3Xqk>7$HRv%Bb01ZQz&(;om@wVfvk7uHhX)*ZP?lJ^gF#MLLP6w zVKIe1KlAr&-~~jJqdC= z?3&|^M|RB_hdiu#h0?cbvmVqtM{8sT^#T=Y{Y;5}C<<~sY~qi`IuKjWrxMRlV&^b1 zd*h~zBf4RcGG3D9Y=jT<0d~nKTN2g`RnZDCOhgmW0DG%U%uqT{bh@TiOf9kyP6%PT zkXBVvqLcHH`2d?Sbc=2Y$!jGdVWm{kimGB24L!12HuTZ@HKOOOf`ns>0XD!scbDSgw|4U(zQwJs8RV?Zj4hcoY5HHHkQDIRy zzYvedV|TbJ3&MXkmQ#Q|~0 rmDqe|H^pg(IB1HxNt?o7JA(M#mDqS<*LcAp4sY$Jw=Pfoxps~FA}d$Z literal 0 HcmV?d00001 diff --git a/submissions/lab4-assets/lab4-trace.txt b/submissions/lab4-assets/lab4-trace.txt new file mode 100644 index 000000000..fb3c477cd --- /dev/null +++ b/submissions/lab4-assets/lab4-trace.txt @@ -0,0 +1,43 @@ +19:46:12.625388 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [S], seq 4277809752, win 65495, options [mss 65495,sackOK,TS val 2900709604 ecr 0,nop,wscale 7], length 0 +E..<.}@.@..<..............2X.........0......... +..P......... +19:46:12.625400 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags [S.], seq 2244450212, ack 4277809753, win 65483, options [mss 65495,sackOK,TS val 2900709604 ecr 2900709604,nop,wscale 7], length 0 +E..<..@.@.<...................2Y.....0......... +..P...P..... +19:46:12.625409 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2900709604 ecr 2900709604], length 0 +E..4.~@.@..C..............2Y.........(..... +..P...P. +19:46:12.625606 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 512, options [nop,nop,TS val 2900709604 ecr 2900709604], length 175: HTTP: POST /notes HTTP/1.1 +E.....@.@.................2Y............... +..P...P.POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/7.82.0 +Accept: */* +Content-Type: application/json +Content-Length: 39 + +{"title":"trace me","body":"in flight"} +19:46:12.625632 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags [.], ack 176, win 511, options [nop,nop,TS val 2900709604 ecr 2900709604], length 0 +E..4..@.@.....................3......(..... +..P...P. +19:46:12.631249 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags [P.], seq 1:206, ack 176, win 512, options [nop,nop,TS val 2900709610 ecr 2900709604], length 205: HTTP: HTTP/1.1 201 Created +E.....@.@..K..................3............ +..P...P.HTTP/1.1 201 Created +Content-Type: application/json +Date: Tue, 16 Jun 2026 16:46:12 GMT +Content-Length: 92 + +{"id":6,"title":"trace me","body":"in flight","created_at":"2026-06-16T16:46:12.62574443Z"} + +19:46:12.631275 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [.], ack 206, win 511, options [nop,nop,TS val 2900709610 ecr 2900709610], length 0 +E..4..@.@..A..............3....r.....(..... +..P...P. +19:46:12.631448 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [F.], seq 176, ack 206, win 512, options [nop,nop,TS val 2900709610 ecr 2900709610], length 0 +E..4..@.@..@..............3....r.....(..... +..P...P. +19:46:12.631508 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags [F.], seq 206, ack 177, win 512, options [nop,nop,TS val 2900709610 ecr 2900709610], length 0 +E..4..@.@..................r..3 .....(..... +..P...P. +19:46:12.631522 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags [.], ack 207, win 512, options [nop,nop,TS val 2900709610 ecr 2900709610], length 0 +E..4..@.@..?..............3 ...s.....(..... +..P...P. diff --git a/submissions/lab4-assets/mtr-localhost.txt b/submissions/lab4-assets/mtr-localhost.txt new file mode 100644 index 000000000..aaa58223c --- /dev/null +++ b/submissions/lab4-assets/mtr-localhost.txt @@ -0,0 +1,4 @@ +$ mtr -rwc 5 localhost +Start: 2026-06-16T19:47:44+0300 +HOST: DESKTOP-I0DHHPT Loss% Snt Last Avg Best Wrst StDev + 1.|-- localhost 0.0% 5 0.0 0.0 0.0 0.0 0.0 diff --git a/submissions/lab4-assets/outsidein-1-process.txt b/submissions/lab4-assets/outsidein-1-process.txt new file mode 100644 index 000000000..98f94174d --- /dev/null +++ b/submissions/lab4-assets/outsidein-1-process.txt @@ -0,0 +1,2 @@ +$ ps -ef | grep quicknotes +root 4535 4460 0 19:48 pts/0 00:00:00 /root/.cache/go-build/01/01b36d0cffeef160fa8af2f501e7375bd20c7b28a5467c110d6e43fef651f8f2-d/quicknotes diff --git a/submissions/lab4-assets/outsidein-2-listening.txt b/submissions/lab4-assets/outsidein-2-listening.txt new file mode 100644 index 000000000..a9aee1a47 --- /dev/null +++ b/submissions/lab4-assets/outsidein-2-listening.txt @@ -0,0 +1,2 @@ +$ ss -tlnp | grep 8080 +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=4535,fd=3)) diff --git a/submissions/lab4-assets/outsidein-3-health.txt b/submissions/lab4-assets/outsidein-3-health.txt new file mode 100644 index 000000000..cc1792283 --- /dev/null +++ b/submissions/lab4-assets/outsidein-3-health.txt @@ -0,0 +1,2 @@ +$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/health +200 diff --git a/submissions/lab4-assets/outsidein-4-firewall.txt b/submissions/lab4-assets/outsidein-4-firewall.txt new file mode 100644 index 000000000..002e4f750 --- /dev/null +++ b/submissions/lab4-assets/outsidein-4-firewall.txt @@ -0,0 +1 @@ +$ sudo iptables -L -n -v 2>/dev/null || sudo nft list ruleset 2>/dev/null || true diff --git a/submissions/lab4-assets/outsidein-5-dns.txt b/submissions/lab4-assets/outsidein-5-dns.txt new file mode 100644 index 000000000..cab07dd95 --- /dev/null +++ b/submissions/lab4-assets/outsidein-5-dns.txt @@ -0,0 +1,2 @@ +$ dig +short localhost +127.0.0.1 diff --git a/submissions/lab4-assets/qn-broken.log b/submissions/lab4-assets/qn-broken.log new file mode 100644 index 000000000..91905670b --- /dev/null +++ b/submissions/lab4-assets/qn-broken.log @@ -0,0 +1,3 @@ +2026/06/16 19:48:28 quicknotes listening on :8080 (notes loaded: 6) +2026/06/16 19:48:28 listen: listen tcp :8080: bind: address already in use +exit status 1 diff --git a/submissions/lab4-assets/repair-health.txt b/submissions/lab4-assets/repair-health.txt new file mode 100644 index 000000000..394f3fdb8 --- /dev/null +++ b/submissions/lab4-assets/repair-health.txt @@ -0,0 +1 @@ +{"notes":6,"status":"ok"} diff --git a/submissions/lab4-assets/ss-8080.txt b/submissions/lab4-assets/ss-8080.txt new file mode 100644 index 000000000..d7c5cb3ae --- /dev/null +++ b/submissions/lab4-assets/ss-8080.txt @@ -0,0 +1,2 @@ +$ ss -tlnp | grep :8080 +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=4337,fd=3)) diff --git a/submissions/lab4.md b/submissions/lab4.md new file mode 100644 index 000000000..e69de29bb From 95583da6beba0505512ddf7a1edaa83ecb794fab Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 19:55:31 +0300 Subject: [PATCH 11/14] docs(lab4): remove lab3 submission Signed-off-by: Maximilian Mifsud Bonici --- submissions/lab3.md | 82 --------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 submissions/lab3.md diff --git a/submissions/lab3.md b/submissions/lab3.md deleted file mode 100644 index d453d3b52..000000000 --- a/submissions/lab3.md +++ /dev/null @@ -1,82 +0,0 @@ -# Lab 3 Submission - -Chosen path: GitHub Actions - -I chose GitHub Actions because the repository is hosted on GitHub and this is the default path for the lab. - -## Task 1 Evidence - -### Failed CI run - -I deliberately introduced a breaking change to prove that the PR gate blocks bad commits. The CI checks failed as expected. - -Failed run screenshot: https://gyazo.com/71cab95128f4fa3e7352b166059ba309 - -### Fixed CI run - -After confirming the gate failed correctly, I reverted/fixed the breaking change and pushed the correction. The CI checks passed again. - -Fixed / green run screenshot: https://gyazo.com/e17d7fcf32959171edd8bb25d9e53be2 - -### Branch protection - -Branch protection was enabled on `main` in my fork. The rule requires status checks to pass before merging, so the PR cannot be merged unless the CI pipeline is green. - -Branch protection screenshot: https://gyazo.com/6681bd00b8b3d6b65055b93f69626a57 - - -## Design Questions - -### a) Why pin the runner version instead of ubuntu-latest? - -Pinning `ubuntu-24.04` makes the CI environment predictable. `ubuntu-latest` can move to a newer Ubuntu image later, which may change installed packages, shell behavior, Go toolchain defaults, or dependency versions. That can break a previously green pipeline without any code change. - -### b) Why split vet, test, and lint into separate units? - -Splitting them gives clearer failure reports and allows the jobs to run in parallel. If everything is in one combined job, one early failure hides later failures, and the CI output becomes slower and less useful. - -### c) What real attack does SHA pinning prevent? - -SHA pinning protects against supply-chain attacks where an attacker compromises or retags a GitHub Action version tag. The lab references the `tj-actions/changed-files` supply-chain incident from March 2025. If a workflow used a mutable tag, it could suddenly execute malicious action code. A full commit SHA points to one exact immutable commit. - -### d) What is permissions: and what principle is behind it? - -`permissions:` controls what the GitHub Actions token is allowed to do inside the workflow. Setting `contents: read` follows least privilege: the workflow only gets the access it needs to read the repository, not broad write permissions it does not need. - -### e) GitLab path: what's the difference between a stage and a job? What would dependencies: do that stages: doesn't? - -A GitLab job is one unit of work, such as running tests or linting. A stage is a group/order level that controls when jobs run; for example, all jobs in the `test` stage can run before jobs in the `deploy` stage. `stages:` controls execution order, while `dependencies:` controls which previous job artifacts are downloaded by a job. So `dependencies:` is about artifact flow, not just job ordering. - -## Task 2: Make It Fast and Smart - -### Optimizations applied - -I enabled Go caching through `actions/setup-go` by setting `cache: true` and using `app/go.mod` as the cache dependency path. Since the Go module is inside `app/`, this makes the cache key depend on the actual module file used by the project. - -I added a Go version matrix for `vet` and `test`, running both jobs on Go `1.23` and Go `1.24`. This helps detect problems that only appear under one Go version. - -I added path filters so the workflow only runs when files inside `app/` or `.github/workflows/ci.yml` change. This prevents documentation-only changes from wasting CI minutes. - -I added a `ci-ok` aggregation job and made it the required branch-protection check. This avoids having to manually require every matrix check name such as `test (1.23)` and `test (1.24)`. - -### Timing table - -| Scenario | Wall-clock | -|----------|-----------:| -| Baseline: no cache, single Go version, no path filter | 1m 11s | -| With cache | 1m 5s | -| With cache + matrix | 1m 9s | - -Caching slightly reduced the total wall-clock time, but the improvement was small because QuickNotes has no third-party dependencies. Most of the workflow time is spent on runner startup, checkout, Go setup, and installing/running tools rather than downloading modules. The matrix version still completed in a similar time because the Go 1.23 and Go 1.24 jobs run in parallel, although it starts more jobs overall. - -### f) Why cache `go.sum`-keyed inputs and not build outputs? - -Caching should be based on deterministic inputs. In a Go project, `go.sum` records exact dependency versions and checksums, so it is a safe basis for deciding whether the module cache can be reused. Build outputs are less safe to cache blindly because they can depend on the operating system, CPU architecture, Go version, compiler flags, and environment. Reusing the wrong build outputs could hide real build problems or make the CI behave inconsistently. - -### g) What does `fail-fast: false` change in a matrix run, and when do you actually want `fail-fast: true`? - -`fail-fast: false` means that if one matrix job fails, GitHub Actions still continues running the other matrix jobs. This is useful here because I want to see whether a failure happens only on Go `1.23`, only on Go `1.24`, or on both versions. `fail-fast: true` is useful when jobs are expensive and one failure already proves the pipeline is unusable, because cancelling the remaining jobs saves time and CI minutes. - -### h) What is the risk of an attacker writing a cache from a malicious PR that protected branches later read? - -The risk is cache poisoning. A malicious pull request could try to store attacker-controlled files in a cache, and then a later trusted workflow might restore that cache and use those files. This is dangerous if the cached content affects the build, test, or release process. GitHub reduces this risk by limiting cache access depending on the branch and event context, but workflows should still avoid caching sensitive files or generated executables from untrusted code. \ No newline at end of file From 305b5f3eb48be2a0466e5a6929731ab30f9e7eef Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 20:07:58 +0300 Subject: [PATCH 12/14] docs(lab4): update networking trace submission Signed-off-by: Maximilian Mifsud Bonici --- submissions/lab4-assets/repair-listening.txt | 1 + submissions/lab4-assets/repair-server.log | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 submissions/lab4-assets/repair-listening.txt create mode 100644 submissions/lab4-assets/repair-server.log diff --git a/submissions/lab4-assets/repair-listening.txt b/submissions/lab4-assets/repair-listening.txt new file mode 100644 index 000000000..635c6d9f7 --- /dev/null +++ b/submissions/lab4-assets/repair-listening.txt @@ -0,0 +1 @@ +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=4886,fd=3)) diff --git a/submissions/lab4-assets/repair-server.log b/submissions/lab4-assets/repair-server.log new file mode 100644 index 000000000..41178da2b --- /dev/null +++ b/submissions/lab4-assets/repair-server.log @@ -0,0 +1,2 @@ +2026/06/16 20:02:09 quicknotes listening on :8080 (notes loaded: 6) +2026/06/16 20:02:33 shutting down From 3ebea4a4a6287395578aec858a2da28a7a111853 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 20:17:06 +0300 Subject: [PATCH 13/14] docs(lab4): remove lab3 CI workflow Signed-off-by: Maximilian Mifsud Bonici --- .github/workflows/ci.yml | 98 ---------------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 65e5a0572..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: ci - -on: - push: - branches: [main] - paths: - - "app/**" - - ".github/workflows/ci.yml" - pull_request: - branches: [main] - paths: - - "app/**" - - ".github/workflows/ci.yml" - -permissions: - contents: read - -jobs: - vet: - name: vet (${{ matrix.go-version }}) - runs-on: ubuntu-24.04 - - strategy: - fail-fast: false - matrix: - go-version: ["1.23", "1.24"] - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version: ${{ matrix.go-version }} - cache: true - cache-dependency-path: app/go.mod - - - name: Run go vet - working-directory: app - run: go vet ./... - - test: - name: test (${{ matrix.go-version }}) - runs-on: ubuntu-24.04 - - strategy: - fail-fast: false - matrix: - go-version: ["1.23", "1.24"] - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version: ${{ matrix.go-version }} - cache: true - cache-dependency-path: app/go.mod - - - name: Run race tests - working-directory: app - run: go test -race -count=1 ./... - - lint: - name: lint - runs-on: ubuntu-24.04 - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version: "1.24" - cache: true - cache-dependency-path: app/go.mod - - - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 - - - name: Run golangci-lint - working-directory: app - run: golangci-lint run - - ci-ok: - name: ci-ok - if: always() - needs: [vet, test, lint] - runs-on: ubuntu-24.04 - - steps: - - name: Check all required jobs passed - run: | - test "${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }}" = "true" \ No newline at end of file From e7ed07bbe883bcb380b2ebefff0f063c1b5a0a88 Mon Sep 17 00:00:00 2001 From: Maximilian Mifsud Bonici Date: Tue, 16 Jun 2026 20:21:23 +0300 Subject: [PATCH 14/14] docs(lab4): add submission contents Signed-off-by: Maximilian Mifsud Bonici --- submissions/lab4.md | 858 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 858 insertions(+) diff --git a/submissions/lab4.md b/submissions/lab4.md index e69de29bb..be24d6751 100644 --- a/submissions/lab4.md +++ b/submissions/lab4.md @@ -0,0 +1,858 @@ +\# Lab 4 Submission + + + +\## Task 1: Trace a Request End-to-End + + + +\### QuickNotes request + + + +I started QuickNotes locally on port `8080` and captured one request to `POST /notes` using `tcpdump` on the loopback interface. + + + +The request was sent with: + + + +```bash + +curl -v -X POST http://localhost:8080/notes \\ + + -H 'Content-Type: application/json' \\ + + -d '{"title":"trace me","body":"in flight"}' + +``` + + + +The application responded successfully: + + + +```text + +POST /notes HTTP/1.1 + +HTTP/1.1 201 Created + +{"id":6,"title":"trace me","body":"in flight","created\_at":"2026-06-16T16:46:12.62574443Z"} + +``` + + + +This confirms that the request reached QuickNotes, QuickNotes created the note, and the server returned a valid JSON response with status `201 Created`. + + + +\--- + + + +\## Annotated Packet Capture + + + +The full decoded capture is included in: + + + +```text + +submissions/lab4-assets/lab4-trace.txt + +``` + + + +\### TCP three-way handshake + + + +The TCP connection starts with the standard three-way handshake: + + + +```text + +19:46:12.625388 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags \[S], seq 4277809752, win 65495, options \[mss 65495,sackOK,TS val 2900709604 ecr 0,nop,wscale 7], length 0 + +19:46:12.625400 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags \[S.], seq 2244450212, ack 4277809753, win 65483, options \[mss 65495,sackOK,TS val 2900709604 ecr 2900709604,nop,wscale 7], length 0 + +19:46:12.625409 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags \[.], ack 1, win 512, options \[nop,nop,TS val 2900709604 ecr 2900709604], length 0 + +``` + + + +Annotation: + + + +\* `SYN`: the client starts a TCP connection to `127.0.0.1:8080`. + +\* `SYN/ACK`: the QuickNotes server accepts and acknowledges the connection. + +\* `ACK`: the client confirms the connection is established. + +\* After this point, HTTP data can be sent over the TCP connection. + + + +\### HTTP request + + + +The HTTP request line and body appear in the capture: + + + +```text + +19:46:12.625606 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags \[P.], seq 1:176, ack 1, win 512, options \[nop,nop,TS val 2900709604 ecr 2900709604], length 175: HTTP: POST /notes HTTP/1.1 + + + +POST /notes HTTP/1.1 + +Host: localhost:8080 + +User-Agent: curl/7.82.0 + +Accept: \*/\* + +Content-Type: application/json + +Content-Length: 39 + + + +{"title":"trace me","body":"in flight"} + +``` + + + +Annotation: + + + +This is the application-layer request. The client sends a `POST` request to `/notes` with a JSON body. The body contains the title and body of the note that should be created. + + + +\### HTTP response + + + +The server response appears in the capture: + + + +```text + +19:46:12.631249 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags \[P.], seq 1:206, ack 176, win 512, options \[nop,nop,TS val 2900709610 ecr 2900709604], length 205: HTTP: HTTP/1.1 201 Created + + + +HTTP/1.1 201 Created + +Content-Type: application/json + +Date: Tue, 16 Jun 2026 16:46:12 GMT + +Content-Length: 92 + + + +{"id":6,"title":"trace me","body":"in flight","created\_at":"2026-06-16T16:46:12.62574443Z"} + +``` + + + +Annotation: + + + +QuickNotes returned `201 Created`, which means the note was successfully created. The response body contains the generated note ID, original title/body, and creation timestamp. + + + +\### Connection close + + + +The capture ends with the TCP connection being closed: + + + +```text + +19:46:12.631448 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags \[F.], seq 176, ack 206, win 512, options \[nop,nop,TS val 2900709610 ecr 2900709610], length 0 + +19:46:12.631508 IP 127.0.0.1.8080 > 127.0.0.1.39140: Flags \[F.], seq 206, ack 177, win 512, options \[nop,nop,TS val 2900709610 ecr 2900709610], length 0 + +19:46:12.631522 IP 127.0.0.1.39140 > 127.0.0.1.8080: Flags \[.], ack 207, win 512, options \[nop,nop,TS val 2900709610 ecr 2900709610], length 0 + +``` + + + +Annotation: + + + +The client sends `FIN`, the server responds with `FIN`, and the client sends the final `ACK`. This is the normal TCP connection teardown after the HTTP exchange completed. + + + +\--- + + + +\## Five Debugging Commands + + + +\### 1. Listening socket + + + +Command: + + + +```bash + +ss -tlnp | grep :8080 + +``` + + + +Output: + + + +```text + +$ ss -tlnp | grep :8080 + +LISTEN 0 4096 \*:8080 \*:\* users:(("quicknotes",pid=4337,fd=3)) + +``` + + + +Decision: + + + +This confirms that QuickNotes is listening on TCP port `8080`. If this command returned no output, then nothing would be bound to the expected port. + + + +\--- + + + +\### 2. Host routes + + + +Command: + + + +```bash + +ip route show + +``` + + + +Output: + + + +```text + +$ ip route show + +default via 172.19.128.1 dev eth0 proto kernel + +172.19.128.0/20 dev eth0 proto kernel scope link src 172.19.130.184 + +``` + + + +Decision: + + + +This shows the host routing table inside WSL. The default route goes through `eth0`, while the QuickNotes request to `localhost` stays on loopback and does not need the external route. + + + +\--- + + + +\### 3. Reachability to localhost + + + +Command: + + + +```bash + +mtr -rwc 5 localhost + +``` + + + +Output: + + + +```text + +$ mtr -rwc 5 localhost + +Start: 2026-06-16T19:47:44+0300 + +HOST: DESKTOP-I0DHHPT Loss% Snt Last Avg Best Wrst StDev + + 1.|-- localhost 0.0% 5 0.0 0.0 0.0 0.0 0.0 + +``` + + + +Decision: + + + +This confirms that `localhost` is reachable with no packet loss. Since this is loopback traffic, the path is one hop and does not depend on the external network. + + + +\--- + + + +\### 4. DNS check + + + +Command: + + + +```bash + +dig +short example.com @1.1.1.1 + +``` + + + +Output: + + + +```text + +$ dig +short example.com @1.1.1.1 + +104.20.23.154 + +172.66.147.243 + +``` + + + +Decision: + + + +This verifies that DNS resolution works through the resolver `1.1.1.1`. It is not directly required for `localhost`, but it confirms that DNS tools and outbound name resolution are working. + + + +\--- + + + +\### 5. User service logs + + + +Command: + + + +```bash + +journalctl --user -u quicknotes -n 20 || true + +``` + + + +Output: + + + +```text + +$ journalctl --user -u quicknotes -n 20 || true + +\-- No entries -- + +``` + + + +Decision: + + + +This checks whether QuickNotes has user-level systemd logs. In my run, QuickNotes was started manually with `go run .`, so a missing `quicknotes` user service log is expected and not itself a failure. + + + +\--- + + + +\## If QuickNotes Returned 502 + + + +If QuickNotes returned `502 Bad Gateway`, I would first check the component in front of QuickNotes, such as a reverse proxy, because `502` usually means the proxy could not successfully talk to the upstream service. I would verify whether QuickNotes is running with `ps`, whether it is listening on the expected port with `ss -tlnp`, and whether it is reachable directly with `curl http://localhost:8080/health`. If the direct health check works, I would inspect the proxy configuration and proxy logs. If the direct health check fails, I would debug the QuickNotes process, port binding, firewall rules, and application logs. + + + +\--- + + + +\# Task 2: Outside-In Debugging on a Broken Deploy + + + +\## Broken Instance Reproduction + + + +I reproduced the broken deployment by starting one QuickNotes process on `:8080`, then trying to start a second QuickNotes process on the same address. + + + +Commands: + + + +```bash + +cd app + +ADDR=:8080 go run . \& + +PID1=$! + +sleep 1 + + + +ADDR=:8080 go run . 2>\&1 | tee ../submissions/lab4-assets/qn-broken.log \& + +PID2=$! + +sleep 2 + + + +ps -ef | grep "go run" | grep -v grep + +``` + + + +Captured error: + + + +```text + +2026/06/16 19:48:27 quicknotes listening on :8080 (notes loaded: 6) + +2026/06/16 19:48:28 quicknotes listening on :8080 (notes loaded: 6) + +2026/06/16 19:48:28 listen: listen tcp :8080: bind: address already in use + +exit status 1 + +``` + + + +Root cause: + + + +```text + +listen tcp :8080: bind: address already in use + +``` + + + +The second QuickNotes instance failed because the first instance was already bound to TCP port `8080`. + + + +\--- + + + +\## Outside-In Debugging Chain + + + +\### 1. Is a process running? + + + +Command: + + + +```bash + +ps -ef | grep quicknotes + +``` + + + +Output: + + + +```text + +$ ps -ef | grep quicknotes + +root 4535 4460 0 19:48 pts/0 00:00:00 /root/.cache/go-build/01/01b36d0cffeef160fa8af2f501e7375bd20c7b28a5467c110d6e43fef651f8f2-d/quicknotes + +``` + + + +Decision: + + + +This confirms that a QuickNotes process exists. Since the process is running, the next step is to check whether it is bound to the expected port. + + + +\--- + + + +\### 2. Is anything listening on port 8080? + + + +Command: + + + +```bash + +ss -tlnp | grep 8080 + +``` + + + +Output: + + + +```text + +$ ss -tlnp | grep 8080 + +LISTEN 0 4096 \*:8080 \*:\* users:(("quicknotes",pid=4535,fd=3)) + +``` + + + +Decision: + + + +This confirms that a process named `quicknotes` is listening on port `8080`. This explains why the second instance could not bind to the same port. + + + +\--- + + + +\### 3. Is the service reachable from the host? + + + +Command: + + + +```bash + +curl -s -o /dev/null -w "%{http\_code}\\n" http://localhost:8080/health + +``` + + + +Output: + + + +```text + +$ curl -s -o /dev/null -w "%{http\_code}\\n" http://localhost:8080/health + +200 + +``` + + + +Decision: + + + +This confirms that something is serving HTTP traffic on `localhost:8080`. The existing instance is healthy, but the new instance failed to start because the port was already occupied. + + + +\--- + + + +\### 4. Is a firewall blocking traffic? + + + +Command: + + + +```bash + +sudo iptables -L -n -v 2>/dev/null || sudo nft list ruleset 2>/dev/null || true + +``` + + + +Output: + + + +```text + +$ sudo iptables -L -n -v 2>/dev/null || sudo nft list ruleset 2>/dev/null || true + +``` + + + +Decision: + + + +This produced no blocking rule output. Since the service is reachable and returns HTTP `200`, the firewall is not the cause of the failure. + + + +\--- + + + +\### 5. Does DNS resolve localhost? + + + +Command: + + + +```bash + +dig +short localhost + +``` + + + +Output: + + + +```text + +$ dig +short localhost + +127.0.0.1 + +``` + + + +Decision: + + + +This confirms that `localhost` resolves to `127.0.0.1`. DNS or host resolution is not the cause of the issue. + + + +\--- + + + +\## Repair and Re-Verification + + + +I killed the conflicting QuickNotes process and restarted QuickNotes on port `8080`. + + + +Commands: + + + +```bash + +pkill -f "go run ." || true + +pkill -f quicknotes || true + +sleep 1 + + + +cd app + +ADDR=:8080 go run . \& + +PID\_FIXED=$! + +sleep 1 + + + +curl -s http://localhost:8080/health + +``` + + + +Output: + + + +```text + +{"notes":6,"status":"ok"} + +``` + + + +After removing the conflicting process, QuickNotes was able to bind to port `8080` and respond to the health check. + + + +\--- + + + +\## Mini-Postmortem + + + +The failure was caused by a port conflict: one QuickNotes instance was already listening on `:8080`, so a second instance could not bind to the same address. This is a systemic deployment problem, not an individual mistake, because deployment scripts and service managers should prevent duplicate instances from silently competing for the same port. Better tooling would include systemd unit management, pre-flight port checks, health checks, and clearer startup failure logging. A deployment pipeline should also verify that the new process actually became healthy before treating the deployment as successful. This would turn the failure from a manual debugging exercise into an automatically detected startup error. + + + +\--- + + + +\## Files Included + + + +```text + +submissions/lab4.md + +submissions/lab4-assets/lab4-trace.pcap + +submissions/lab4-assets/lab4-trace.txt + +submissions/lab4-assets/curl-post.txt + +submissions/lab4-assets/ss-8080.txt + +submissions/lab4-assets/ip-route.txt + +submissions/lab4-assets/mtr-localhost.txt + +submissions/lab4-assets/dig-example.txt + +submissions/lab4-assets/journalctl-quicknotes.txt + +submissions/lab4-assets/qn-broken.log + +submissions/lab4-assets/go-run-processes.txt + +submissions/lab4-assets/outsidein-1-process.txt + +submissions/lab4-assets/outsidein-2-listening.txt + +submissions/lab4-assets/outsidein-3-health.txt + +submissions/lab4-assets/outsidein-4-firewall.txt + +submissions/lab4-assets/outsidein-5-dns.txt + +submissions/lab4-assets/repair-health.txt + +``` + + +