feat(course): build out all 27 modules, capstone, scaffold, and conventions
Scaffold the course repo and author the full curriculum in dependency-chain order, following the settled build decisions in handoff.md. - Scaffold: course README, vendor-neutral AGENTS.md (dogfoods Module 5), _TEMPLATE.md (the fixed 9-section module shape), root .gitignore, ship config. - Modules 1-2: reference exemplars (locked for tone/depth/lab style). - Modules 3-27: full lessons + runnable labs, each following the template, respecting the chain, vendor/model-agnostic, with "feel the pain" labs. - Module 8 hosting comparison web-researched and date-stamped (as of 2026-06-22), not written from memory; expansion-zone modules carry Verify-before-publish. - Capstone: the full loop end to end on the running tasks-app example. Lab code syntax-checked (Python/shell/YAML); every module has the 7 core template sections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# Module 19 lab — what a CI job could see if it ran on THIS machine.
|
||||
#
|
||||
# Run this on any machine you'd consider turning into a self-hosted runner (your laptop is fine for
|
||||
# the exercise). It does NOT change anything — it only LOOKS. The point is to make concrete what is
|
||||
# otherwise abstract: a "workflow step" is just a shell command, so whatever this read-only script
|
||||
# can see, a malicious workflow step (e.g. from a pull request) running on this runner can see too.
|
||||
#
|
||||
# bash inspect-runner.sh
|
||||
#
|
||||
# Then paste the output into your AI and ask it to rank, worst-first, what a malicious PR could
|
||||
# steal or reach if this were your runner. That conversation IS the security tradeoff for this module.
|
||||
|
||||
set -u
|
||||
|
||||
line() { printf '\n=== %s ===\n' "$1"; }
|
||||
|
||||
line "WHO AND WHERE"
|
||||
echo "hostname : $(hostname 2>/dev/null)"
|
||||
echo "user : $(whoami 2>/dev/null) (root? $( [ "$(id -u 2>/dev/null)" = 0 ] && echo YES || echo no ))"
|
||||
echo "os : $(uname -srm 2>/dev/null)"
|
||||
echo " >> A runner should run as a dedicated low-privilege user, never root, never your login."
|
||||
|
||||
line "SECRETS SITTING IN THE ENVIRONMENT"
|
||||
# Don't print values — just the names. Seeing the NAMES is enough to make the point.
|
||||
env | grep -iE 'token|secret|key|password|passwd|credential|aws|gcp|azure|api' | cut -d= -f1 | sort -u \
|
||||
| sed 's/^/ exposed env var: /' || true
|
||||
echo " >> Any of these is readable by every job step. Scope runner secrets to the absolute minimum."
|
||||
|
||||
line "CREDENTIAL FILES ON DISK"
|
||||
for p in \
|
||||
"$HOME/.aws/credentials" \
|
||||
"$HOME/.config/gcloud" \
|
||||
"$HOME/.azure" \
|
||||
"$HOME/.docker/config.json" \
|
||||
"$HOME/.kube/config" \
|
||||
"$HOME/.netrc" \
|
||||
"$HOME/.git-credentials" ; do
|
||||
[ -e "$p" ] && echo " FOUND: $p"
|
||||
done
|
||||
echo " (nothing listed above = none of those common credential stores are present here)"
|
||||
|
||||
line "SSH KEYS (pivot material)"
|
||||
if [ -d "$HOME/.ssh" ]; then
|
||||
ls -1 "$HOME/.ssh" 2>/dev/null | sed 's/^/ ~\/.ssh\//'
|
||||
echo " >> Private keys here let a compromised job hop to every host you can SSH to."
|
||||
else
|
||||
echo " no ~/.ssh directory"
|
||||
fi
|
||||
|
||||
line "DOCKER SOCKET (root-equivalent if present)"
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
echo " /var/run/docker.sock EXISTS and is reachable."
|
||||
echo " >> Access to the Docker socket is effectively root on the host. Big deal."
|
||||
else
|
||||
echo " no reachable docker socket"
|
||||
fi
|
||||
|
||||
line "PRIVATE NETWORK REACH (the reason you self-host — and the reason it's dangerous)"
|
||||
# Probe a few common private ranges' gateways and any hosts you care about.
|
||||
# Edit these to match your network for a sharper result.
|
||||
PROBES=( "192.168.0.1:80" "192.168.1.1:80" "10.0.0.1:80" )
|
||||
for hp in "${PROBES[@]}"; do
|
||||
host="${hp%%:*}"; port="${hp##*:}"
|
||||
if timeout 2 bash -c ">/dev/tcp/${host}/${port}" 2>/dev/null; then
|
||||
echo " REACHABLE: ${host}:${port}"
|
||||
fi
|
||||
done
|
||||
echo " (edit the PROBES list above to test your real internal hosts — databases, deploy targets)"
|
||||
echo " >> Every reachable internal host is something a compromised runner can attack or exfiltrate."
|
||||
|
||||
line "BOTTOM LINE"
|
||||
echo "Everything listed above is what a self-hosted runner on this box would hand to ANY job it runs,"
|
||||
echo "including a job defined by a pull request you haven't merged. That is the tradeoff. Mitigate with:"
|
||||
echo " - ephemeral runners (fresh environment per job)"
|
||||
echo " - a dedicated low-priv user on an isolated network segment"
|
||||
echo " - least-privilege secrets, and NEVER attach this to a public repo without fork-PR approval"
|
||||
@@ -0,0 +1,73 @@
|
||||
# Module 19 lab — "Where did this actually run?"
|
||||
#
|
||||
# This is the Module 14 CI pipeline (lint + test the tasks-app) with one extra step bolted on the
|
||||
# end: it makes the runner tell you who and where it is. Run it once on a hosted runner, then again
|
||||
# after you've pointed it at your own self-hosted runner in Track B, and compare the two receipts.
|
||||
#
|
||||
# Where this file goes: the same workflow directory as your Module 14 ci.yml. On Actions-style forges
|
||||
# (GitHub, and Forgejo/Gitea with Actions-compatible YAML) that's <forge-dir>/workflows/ at the repo
|
||||
# root — e.g. .github/workflows/whoami-runner.yml. The filename is yours; the directory is not.
|
||||
#
|
||||
# For GitLab CI, the same idea is a one-job .gitlab-ci.yml: run the same script lines under `script:`
|
||||
# with `tags:` selecting your runner. The shape rhymes; only the YAML dialect changes.
|
||||
|
||||
name: whoami-runner
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch: # lets you trigger it by hand from the forge UI
|
||||
|
||||
jobs:
|
||||
whoami:
|
||||
# Track A: leave this as the hosted image and read the receipt.
|
||||
# Track B: change this to select your own runner by label, e.g.
|
||||
# runs-on: [self-hosted, linux]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install tools
|
||||
run: pip install pytest ruff
|
||||
|
||||
# The real Module 14 checks still run — a self-hosted runner has to actually do the work.
|
||||
- name: Lint
|
||||
run: ruff check .
|
||||
|
||||
- name: Test
|
||||
run: pytest -q
|
||||
|
||||
# The point of THIS workflow: make the runner identify itself.
|
||||
- name: Where did this run?
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=== runner identity ==="
|
||||
echo "hostname : $(hostname)"
|
||||
echo "os : $(uname -a)"
|
||||
echo "user : $(whoami)"
|
||||
echo "workdir : $(pwd)"
|
||||
echo
|
||||
echo "=== ephemeral? (does junk from a previous run survive?) ==="
|
||||
MARK="$HOME/.module19_ran_before"
|
||||
if [ -f "$MARK" ]; then
|
||||
echo "FOUND a marker from a PREVIOUS run at $MARK"
|
||||
echo " -> this machine is PERSISTENT (not a fresh throwaway). Expect a self-hosted runner."
|
||||
else
|
||||
echo "No marker found. Either this is a fresh machine (hosted) or the first run here."
|
||||
fi
|
||||
date > "$MARK" 2>/dev/null && echo "(left a marker for next time)" || echo "(could not write a marker)"
|
||||
echo
|
||||
echo "=== can this runner reach the public internet? ==="
|
||||
if curl -fsS -m 5 https://example.com >/dev/null 2>&1; then
|
||||
echo "YES — outbound internet works from here."
|
||||
else
|
||||
echo "NO — no outbound internet (could be an air-gapped / isolated runner)."
|
||||
fi
|
||||
echo
|
||||
echo "Now ask: is this machine MINE, and what else can it reach? (see inspect-runner.sh)"
|
||||
Reference in New Issue
Block a user