diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..aedccd057 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..78551f834 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: release + +# Task 1 — on a semver tag, build QuickNotes and publish it to ghcr.io. +on: + push: + tags: + - "v*" + +# Least privilege: read the repo, write packages (ghcr). Nothing else. +permissions: + contents: read + packages: write + +jobs: + build-push: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 + + - name: Log in to ghcr.io + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} # auto-issued, packages:write only + + - name: Derive image tags (semver + latest) + id: meta + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 + with: + images: ghcr.io/${{ github.repository }}/quicknotes + tags: | + type=raw,value=${{ github.ref_name }} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + with: + context: ./app + platforms: linux/amd64 # Hugging Face Spaces runs amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..3f273792c --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 + +# ---- builder --------------------------------------------------------------- +FROM golang:1.25 AS builder +WORKDIR /src + +# Copy the module manifest first so `go mod download` is cached independently +# of the source — edits to .go files don't bust the dependency layer. +COPY go.mod ./ +RUN go mod download + +# Now the source. +COPY . . + +# Static, stripped, reproducible binary. CGO off so distroless-static (which has +# no libc / dynamic linker) can run it. +RUN CGO_ENABLED=0 GOOS=linux GOTOOLCHAIN=local \ + go build -trimpath -ldflags='-s -w' -o /out/quicknotes . + +# A /data dir owned by the nonroot uid, so a fresh named volume mounted here +# inherits writable ownership instead of root-owned. +RUN mkdir -p /data && chown 65532:65532 /data + +# ---- runtime --------------------------------------------------------------- +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /out/quicknotes /quicknotes +COPY --from=builder /src/seed.json /seed.json +COPY --from=builder --chown=65532:65532 /data /data + +ENV ADDR=:8080 \ + DATA_PATH=/data/notes.json \ + SEED_PATH=/seed.json + +EXPOSE 8080 +USER nonroot:nonroot +ENTRYPOINT ["/quicknotes"] diff --git a/app/handlers.go b/app/handlers.go index c534979c5..08f22c5b4 100644 --- a/app/handlers.go +++ b/app/handlers.go @@ -26,7 +26,7 @@ func NewServer(store *Store) *Server { return &Server{store: store, requestsByCode: by} } -func (s *Server) Routes() *http.ServeMux { +func (s *Server) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.wrap(s.handleHealth)) mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics)) @@ -34,7 +34,24 @@ func (s *Server) Routes() *http.ServeMux { mux.HandleFunc("POST /notes", s.wrap(s.handleCreateNote)) mux.HandleFunc("GET /notes/{id}", s.wrap(s.handleGetNote)) mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote)) - return mux + // Security headers applied once, to every route (Lab 9 — ZAP hardening). + return securityHeaders(mux) +} + +// securityHeaders wraps the whole router so every response carries a consistent +// set of hardening headers. QuickNotes is a JSON API, so `default-src 'none'` +// (the strictest CSP) is safe — the app serves no HTML/JS/CSS that a CSP could +// break — and no-store keeps note data out of shared caches. +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("X-Content-Type-Options", "nosniff") + h.Set("Content-Security-Policy", "default-src 'none'") + h.Set("X-Frame-Options", "DENY") + h.Set("Referrer-Policy", "no-referrer") + h.Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) } type statusWriter struct { diff --git a/app/handlers_test.go b/app/handlers_test.go index 9dff2e3e5..8f3dceaaa 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -131,3 +131,22 @@ func TestMetrics_ExposesPrometheusFormat(t *testing.T) { } } +func TestSecurityHeaders_PresentOnEveryResponse(t *testing.T) { + srv := newTestServer(t) + // Hit an ordinary route; the headers come from the router-level middleware, + // so this fails if securityHeaders() is removed from Routes(). + rec := do(t, srv, http.MethodGet, "/health", nil) + want := map[string]string{ + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": "default-src 'none'", + "X-Frame-Options": "DENY", + "Referrer-Policy": "no-referrer", + "Cache-Control": "no-store", + } + for k, v := range want { + if got := rec.Header().Get(k); got != v { + t.Errorf("header %s = %q, want %q", k, got, v) + } + } +} + diff --git a/app/main.go b/app/main.go index e258ffcfe..76d22ee46 100644 --- a/app/main.go +++ b/app/main.go @@ -7,11 +7,19 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" ) func main() { + // Self-healthcheck mode: the distroless runtime image has no shell or curl, + // so the container's HEALTHCHECK re-invokes this same binary (the only one in + // the image) to probe /health. Exit 0 = healthy, 1 = unhealthy. + if len(os.Args) > 1 && os.Args[1] == "healthcheck" { + os.Exit(healthcheck()) + } + addr := envOrDefault("ADDR", ":8080") dataPath := envOrDefault("DATA_PATH", "data/notes.json") seedPath := envOrDefault("SEED_PATH", "seed.json") @@ -51,6 +59,24 @@ func main() { } } +func healthcheck() int { + addr := envOrDefault("ADDR", ":8080") + host := addr + if strings.HasPrefix(addr, ":") { + host = "127.0.0.1" + addr + } + client := &http.Client{Timeout: 2 * time.Second} + resp, err := client.Get("http://" + host + "/health") + if err != nil { + return 1 + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return 1 + } + return 0 +} + func envOrDefault(k, def string) string { if v, ok := os.LookupEnv(k); ok && v != "" { return v diff --git a/cloud/Dockerfile b/cloud/Dockerfile new file mode 100644 index 000000000..5364059e9 --- /dev/null +++ b/cloud/Dockerfile @@ -0,0 +1,10 @@ +# Hugging Face Space (Docker SDK) — serve the exact image the release CI pushed +# to ghcr.io. We PULL the immutable tag rather than rebuild from source, so the +# Space runs the same artifact CI built and Lab 9 scanned (reproducible, no build +# drift). Trade-off discussed in design question (f). +FROM ghcr.io/rikire/devops-intro/quicknotes:v0.1.0 + +# The base image already: runs as nonroot (uid 65532), ENTRYPOINT ["/quicknotes"], +# listens on :8080 (0.0.0.0), and ships seed data. README.md frontmatter sets +# `app_port: 8080` so HF routes the public URL to this port. +EXPOSE 8080 diff --git a/cloud/README.md b/cloud/README.md new file mode 100644 index 000000000..a1615ea9c --- /dev/null +++ b/cloud/README.md @@ -0,0 +1,29 @@ +--- +title: QuickNotes +emoji: 📝 +colorFrom: blue +colorTo: green +sdk: docker +app_port: 8080 +pinned: false +--- + +# QuickNotes on Hugging Face Spaces + +This Space runs the QuickNotes container image published to +`ghcr.io/rikire/devops-intro/quicknotes` by the release CI (Lab 10, Task 1). + +The `app_port: 8080` line in the YAML frontmatter above is required: Hugging Face +defaults to port **7860**, but QuickNotes listens on **8080**, so we tell HF to +route the public URL to 8080. + +## Endpoints + +- `GET /health` → `{"status":"ok","notes":N}` +- `GET /notes` → list notes +- `POST /notes` → create a note +- `GET /metrics` → Prometheus metrics + +> This `README.md` and the sibling `Dockerfile` are the two files pushed to the +> Space's own Git repo. They are kept here under `cloud/` for the course +> submission. diff --git a/cloud/teardown.md b/cloud/teardown.md new file mode 100644 index 000000000..89f0642b6 --- /dev/null +++ b/cloud/teardown.md @@ -0,0 +1,17 @@ +# Teardown — Lab 10 + +Both targets cost $0, but here is how to remove everything. + +## Hugging Face Space +- Space → **Settings** → **Delete this Space** (bottom of the page). This removes + the container and the public `*.hf.space` URL. Nothing else is billed. + +## ghcr.io image +- Your GitHub profile → **Packages** → `quicknotes` → **Package settings** → + **Delete this package** (or **Manage versions** to delete a single tag). +- To stop future publishes, delete the git tag: `git push origin :refs/tags/v0.1.0`. + +## Cloudflare quick tunnel (Bonus) +- Just stop the `cloudflared` process (Ctrl-C). Quick tunnels are ephemeral — + the `*.trycloudflare.com` URL disappears when the process ends. No account, + nothing to clean up. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..caed44e3a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,36 @@ +services: + quicknotes: + build: + context: ./app + image: quicknotes:lab6 + ports: + - "8080:8080" + environment: + ADDR: ":8080" + DATA_PATH: /data/notes.json + SEED_PATH: /seed.json + volumes: + - quicknotes-data:/data + restart: unless-stopped + + # --- Hardening: the 6 security defaults (Lecture 6) --- + read_only: true # 4. read-only root filesystem + tmpfs: + - /tmp # ...with a writable tmpfs (the /data volume stays writable) + cap_drop: + - ALL # 3. drop every Linux capability (QuickNotes needs none) + security_opt: + - no-new-privileges:true # 5. block setuid privilege escalation + # (1) USER nonroot and (2) distroless base come from app/Dockerfile. + # (6) Trivy scan documented in submissions/lab6.md. + + healthcheck: + # Distroless has no shell/curl — re-invoke the app binary's healthcheck mode. + test: ["CMD", "/quicknotes", "healthcheck"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + +volumes: + quicknotes-data: diff --git a/security/sbom.cdx.json b/security/sbom.cdx.json new file mode 100644 index 000000000..952505a4b --- /dev/null +++ b/security/sbom.cdx.json @@ -0,0 +1,453 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:b68e954b-5628-4bc5-bce0-6b04d68eeaa7", + "version": 1, + "metadata": { + "timestamp": "2026-07-03T14:19:59+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.59.1" + } + ] + }, + "component": { + "bom-ref": "7d90f63d-0a13-4be7-b3ec-2207b19e6fb2", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:065706fa87545e68426eb8f984f0f160603009f4ae7a26608bb63e6e587e98e2" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:151e3fc65a977c0d5876bcae809305d01e9a18d2fc52aecb4403096bcfe01799" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:187cfc6d1e3e8a40a5e64653bcd3239c140807dcf1c09e48021178705a5a6139" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4cde6b0bb6f50a5f255eef7b2a42162c661cf776b803225dcac9a659e396bb6b" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:4d049f83d9cf21d1f5cc0e11deaf36df02790d0e60c1a3829538fb4b61685368" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:5fd2536c39c0700be8b7b4344e375196da2f126842fd8ede66996a18860a3890" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:6f1cdceb6a3146f0ccb986521156bef8a422cdbb0863396f7f751f575ba308f4" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ad51d0769d16ba578106a177987dfe3d2e02c1668c852b795b2f6b024068242a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:af5aa97ebe6ce1604747ec1e21af7136ded391bcabe4acef882e718a87c86bcc" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bd3cdfae1d3fdd83a2231d608969b38b82349777c2fff9a7c12d54f8ac5c9b38" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ebdcb6b6517675527c74845fcc166d66016cdc237ef7db73884d2dd4f0b73e82" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:a5c3cebfcfee8149d30a8e2f34a14f72fbc0216ba075a6af9bf93aab5d2bb4ed" + }, + { + "name": "aquasecurity:trivy:Labels:com.docker.compose.project", + "value": "devops-intro" + }, + { + "name": "aquasecurity:trivy:Labels:com.docker.compose.service", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:Labels:com.docker.compose.version", + "value": "2.40.3" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "quicknotes:lab6" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "6268bbd2-8e2e-4b83-8b38-8e9a84993e55", + "type": "operating-system", + "name": "debian", + "version": "13.5", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "e372c9a1-d415-44b7-9430-043418fa4170", + "type": "application", + "name": "quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:92cb9c37b7d3957ac56645a979418f65e6c5bdba00eb99622affae5fc124ac07" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Mime-Support Packagers " + }, + "name": "media-types", + "version": "13.0.0", + "licenses": [ + { + "license": { + "name": "ad-hoc" + } + } + ], + "purl": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:275a30dd8ce958b21daa9ad962c6fbc09f98306ee2f486b65c9075dc257b1412" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "media-types@13.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "media-types" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.0.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "name": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:621c35e751a51a9a9dc3e80aa0b7fe8be2a93402ea6ccd307d30852cd7776cda" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata-legacy", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:bec7e6bb35e05d1284f28b10d2150c259717d91c658c4c10c08424bb9466caba" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata-legacy@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:c8b007d0206e4b10ed4d3b3d99dfeab47c2648e82011989fd78a5731baf33fc3" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:golang/quicknotes", + "type": "library", + "name": "quicknotes", + "purl": "pkg:golang/quicknotes", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:151e3fc65a977c0d5876bcae809305d01e9a18d2fc52aecb4403096bcfe01799" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "quicknotes" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/stdlib@v1.24.13", + "type": "library", + "name": "stdlib", + "version": "v1.24.13", + "purl": "pkg:golang/stdlib@v1.24.13", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:151e3fc65a977c0d5876bcae809305d01e9a18d2fc52aecb4403096bcfe01799" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.24.13" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + } + ], + "dependencies": [ + { + "ref": "6268bbd2-8e2e-4b83-8b38-8e9a84993e55", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5" + ] + }, + { + "ref": "7d90f63d-0a13-4be7-b3ec-2207b19e6fb2", + "dependsOn": [ + "6268bbd2-8e2e-4b83-8b38-8e9a84993e55", + "e372c9a1-d415-44b7-9430-043418fa4170" + ] + }, + { + "ref": "e372c9a1-d415-44b7-9430-043418fa4170", + "dependsOn": [ + "pkg:golang/quicknotes" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u5?arch=amd64&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/media-types@13.0.0?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata-legacy@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.5", + "dependsOn": [] + }, + { + "ref": "pkg:golang/quicknotes", + "dependsOn": [ + "pkg:golang/stdlib@v1.24.13" + ] + }, + { + "ref": "pkg:golang/stdlib@v1.24.13", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/security/trivy-config.txt b/security/trivy-config.txt new file mode 100644 index 000000000..be6e7861f --- /dev/null +++ b/security/trivy-config.txt @@ -0,0 +1,14 @@ + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +════════════════════════════════════════ +You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers. + +See https://avd.aquasec.com/misconfig/ds026 +──────────────────────────────────────── + + diff --git a/security/trivy-fs.txt b/security/trivy-fs.txt new file mode 100644 index 000000000..af0c50234 --- /dev/null +++ b/security/trivy-fs.txt @@ -0,0 +1,26 @@ +2026-07-03T14:16:34Z INFO [vulndb] Need to update DB +2026-07-03T14:16:34Z INFO [vulndb] Downloading vulnerability DB... +2026-07-03T14:16:34Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-03T14:16:48Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-03T14:16:48Z INFO [vuln] Vulnerability scanning is enabled +2026-07-03T14:16:48Z INFO [secret] Secret scanning is enabled +2026-07-03T14:16:48Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-03T14:16:48Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-03T14:16:49Z INFO Number of language-specific files num=1 +2026-07-03T14:16:49Z INFO [gomod] Detecting vulnerabilities... + +.vagrant/machines/default/hyperv/private_key (secrets) +====================================================== +Total: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: AsymmetricPrivateKey (private-key) +════════════════════════════════════════ +Asymmetric Private Key +──────────────────────────────────────── + .vagrant/machines/default/hyperv/private_key:1 +──────────────────────────────────────── + 1 [ BEGIN OPENSSH PRIVATE KEY-----*******************************************************************************************************************************************************************************************************************************************************************************************************************************************-----END OPENSSH PRI + 2 +──────────────────────────────────────── + + diff --git a/security/trivy-image-after.txt b/security/trivy-image-after.txt new file mode 100644 index 000000000..880863e9e --- /dev/null +++ b/security/trivy-image-after.txt @@ -0,0 +1,5 @@ + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + diff --git a/security/trivy-image.txt b/security/trivy-image.txt new file mode 100644 index 000000000..a65852065 --- /dev/null +++ b/security/trivy-image.txt @@ -0,0 +1,56 @@ + +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + + +quicknotes (gobinary) +===================== +Total: 11 (HIGH: 11, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬─────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼─────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2026-25679 │ HIGH │ fixed │ v1.24.13 │ 1.25.8, 1.26.1 │ net/url: Incorrect parsing of IPv6 host literals in net/url │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-25679 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-27145 │ │ │ │ 1.25.11, 1.26.4 │ crypto/x509: golang: golang crypto/x509: Denial of Service │ +│ │ │ │ │ │ │ via excessive processing of DNS... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-27145 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32280 │ │ │ │ 1.25.9, 1.26.2 │ crypto/x509: crypto/tls: golang: Go: Denial of Service │ +│ │ │ │ │ │ │ vulnerability in certificate chain building... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32280 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32281 │ │ │ │ │ crypto/x509: golang: Go crypto/x509: Denial of Service via │ +│ │ │ │ │ │ │ inefficient certificate chain validation... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32281 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-32283 │ │ │ │ │ crypto/tls: golang: Go crypto/tls: Denial of Service via │ +│ │ │ │ │ │ │ multiple TLS 1.3 key... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-32283 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33811 │ │ │ │ 1.25.10, 1.26.3 │ net: golang: Go net package: Denial of Service via long │ +│ │ │ │ │ │ │ CNAME response... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33811 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-33814 │ │ │ │ │ net/http/internal/http2: golang: golang.org/x/net: Go │ +│ │ │ │ │ │ │ HTTP/2: Denial of Service via malformed │ +│ │ │ │ │ │ │ SETTINGS_MAX_FRAME_SIZE frame... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-33814 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39820 │ │ │ │ │ net/mail: golang: Go net/mail: Denial of Service via crafted │ +│ │ │ │ │ │ │ email inputs │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39820 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-39836 │ │ │ │ │ ELSA-2026-22121: golang security update (IMPORTANT) │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-39836 │ +│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42499 │ │ │ │ │ net/mail: golang: net/mail: Denial of Service via │ +│ │ │ │ │ │ │ pathological email address parsing │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42499 │ +│ ├────────────────┤ │ │ ├─────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2026-42504 │ │ │ │ 1.25.11, 1.26.4 │ Decoding a maliciously-crafted MIME header containing many │ +│ │ │ │ │ │ │ invalid enc ... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-42504 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────┴──────────────────────────────────────────────────────────────┘ diff --git a/security/zap-after.html b/security/zap-after.html new file mode 100644 index 000000000..66ec477a2 --- /dev/null +++ b/security/zap-after.html @@ -0,0 +1,724 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:8080 + +

+ +

+ Generated on Fri, 3 Jul 2026 15:56:49 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
2
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
Insufficient Site Isolation Against Spectre VulnerabilityLow1
ZAP is Out of DateLow1
Non-Storable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
Insufficient Site Isolation Against Spectre Vulnerability
Description +
Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
ParameterCross-Origin-Resource-Policy
Attack
Evidence
Other Info
Instances1
Solution +
Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.
+
+ +
'same-site' is considered as less secured and should be avoided.
+
+ +
If resources must be shared, set the header to 'cross-origin'.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).
+ +
Reference + https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy + +
CWE Id693
WASC Id14
Plugin Id90004
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://host.docker.internal:8080/robots.txt
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Non-Storable Content
Description +
The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
URLhttp://host.docker.internal:8080/robots.txt
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
URLhttp://host.docker.internal:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidenceno-store
Other Info
Instances3
Solution +
The content may be marked as storable by ensuring that the following conditions are satisfied:
+
+ +
The request method must be understood by the cache and defined as being cacheable ("GET", "HEAD", and "POST" are currently defined as cacheable)
+
+ +
The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
+
+ +
The "no-store" cache directive must not appear in the request or response header fields
+
+ +
For caching by "shared" caches such as "proxy" caches, the "private" response directive must not appear in the response
+
+ +
For caching by "shared" caches such as "proxy" caches, the "Authorization" header field must not appear in the request, unless the response explicitly allows it (using one of the "must-revalidate", "public", or "s-maxage" Cache-Control response directives)
+
+ +
In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
+
+ +
It must contain an "Expires" header field
+
+ +
It must contain a "max-age" response directive
+
+ +
For "shared" caches such as "proxy" caches, it must contain a "s-maxage" response directive
+
+ +
It must contain a "Cache Control Extension" that allows it to be cached
+
+ +
It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/zap-after.json b/security/zap-after.json new file mode 100644 index 000000000..641d06d96 --- /dev/null +++ b/security/zap-after.json @@ -0,0 +1,129 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Fri, 3 Jul 2026 15:56:49", + "created": "2026-07-03T15:56:49.566770032Z", + "site":[ + { + "@name": "http://host.docker.internal:8080", + "@host": "host.docker.internal", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "90004", + "alertRef": "90004-1", + "alert": "Insufficient Site Isolation Against Spectre Vulnerability", + "name": "Insufficient Site Isolation Against Spectre Vulnerability", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.

", + "instances":[ + { + "id": "7", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "Cross-Origin-Resource-Policy", + "attack": "", + "evidence": "", + "otherinfo": "" + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.

'same-site' is considered as less secured and should be avoided.

If resources must be shared, set the header to 'cross-origin'.

If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).

", + "otherinfo": "", + "reference": "

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy

", + "cweid": "693", + "wascid": "14", + "sourceid": "11" + }, + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

The version of ZAP you are using to test your app is out of date and is no longer being updated.

The risk level is set based on how out of date your ZAP version is.

", + "instances":[ + { + "id": "3", + "uri": "http://host.docker.internal:8080/robots.txt", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "The latest version of ZAP is 2.17.0" + } + ], + "count": "1", + "systemic": false, + "solution": "

Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

https://www.zaproxy.org/download/

", + "cweid": "1104", + "wascid": "45", + "sourceid": "8" + }, + { + "pluginid": "10049", + "alertRef": "10049-1", + "alert": "Non-Storable Content", + "name": "Non-Storable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.

", + "instances":[ + { + "id": "5", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "4", + "uri": "http://host.docker.internal:8080/robots.txt", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + }, + { + "id": "0", + "uri": "http://host.docker.internal:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "no-store", + "otherinfo": "" + } + ], + "count": "3", + "systemic": false, + "solution": "

The content may be marked as storable by ensuring that the following conditions are satisfied:

The request method must be understood by the cache and defined as being cacheable (\"GET\", \"HEAD\", and \"POST\" are currently defined as cacheable)

The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)

The \"no-store\" cache directive must not appear in the request or response header fields

For caching by \"shared\" caches such as \"proxy\" caches, the \"private\" response directive must not appear in the response

For caching by \"shared\" caches such as \"proxy\" caches, the \"Authorization\" header field must not appear in the request, unless the response explicitly allows it (using one of the \"must-revalidate\", \"public\", or \"s-maxage\" Cache-Control response directives)

In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:

It must contain an \"Expires\" header field

It must contain a \"max-age\" response directive

For \"shared\" caches such as \"proxy\" caches, it must contain a \"s-maxage\" response directive

It must contain a \"Cache Control Extension\" that allows it to be cached

It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).

", + "otherinfo": "", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "11" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/security/zap-baseline.html b/security/zap-baseline.html new file mode 100644 index 000000000..ea97f52f3 --- /dev/null +++ b/security/zap-baseline.html @@ -0,0 +1,598 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:8080 + +

+ +

+ Generated on Fri, 3 Jul 2026 14:35:40 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
1
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
ZAP is Out of DateLow1
Storable and Cacheable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://host.docker.internal:8080
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Storable and Cacheable Content
Description +
The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where "shared" caching servers such as "proxy" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.
+ +
URLhttp://host.docker.internal:8080
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/robots.txt
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
Instances3
Solution +
Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:
+
+ +
Cache-Control: no-cache, no-store, must-revalidate, private
+
+ +
Pragma: no-cache
+
+ +
Expires: 0
+
+ +
This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/zap-baseline.json b/security/zap-baseline.json new file mode 100644 index 000000000..c0b97a0e4 --- /dev/null +++ b/security/zap-baseline.json @@ -0,0 +1,99 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Fri, 3 Jul 2026 14:35:40", + "created": "2026-07-03T14:35:40.293374209Z", + "site":[ + { + "@name": "http://host.docker.internal:8080", + "@host": "host.docker.internal", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

The version of ZAP you are using to test your app is out of date and is no longer being updated.

The risk level is set based on how out of date your ZAP version is.

", + "instances":[ + { + "id": "3", + "uri": "http://host.docker.internal:8080", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "The latest version of ZAP is 2.17.0" + } + ], + "count": "1", + "systemic": false, + "solution": "

Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

https://www.zaproxy.org/download/

", + "cweid": "1104", + "wascid": "45", + "sourceid": "7" + }, + { + "pluginid": "10049", + "alertRef": "10049-3", + "alert": "Storable and Cacheable Content", + "name": "Storable and Cacheable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where \"shared\" caching servers such as \"proxy\" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.

", + "instances":[ + { + "id": "4", + "uri": "http://host.docker.internal:8080", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "0", + "uri": "http://host.docker.internal:8080/robots.txt", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "1", + "uri": "http://host.docker.internal:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + } + ], + "count": "3", + "systemic": false, + "solution": "

Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:

Cache-Control: no-cache, no-store, must-revalidate, private

Pragma: no-cache

Expires: 0

This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.

", + "otherinfo": "

In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.

", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "7" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/security/zap-before.html b/security/zap-before.html new file mode 100644 index 000000000..14e9a7f70 --- /dev/null +++ b/security/zap-before.html @@ -0,0 +1,806 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:8080 + +

+ +

+ Generated on Fri, 3 Jul 2026 15:53:07 +

+ +

+ ZAP Version: 2.16.1 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
3
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + +

Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
Insufficient Site Isolation Against Spectre VulnerabilityLow1
X-Content-Type-Options Header MissingLow1
ZAP is Out of DateLow1
Storable and Cacheable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
Insufficient Site Isolation Against Spectre Vulnerability
Description +
Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
ParameterCross-Origin-Resource-Policy
Attack
Evidence
Other Info
Instances1
Solution +
Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.
+
+ +
'same-site' is considered as less secured and should be avoided.
+
+ +
If resources must be shared, set the header to 'cross-origin'.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).
+ +
Reference + https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy + +
CWE Id693
WASC Id14
Plugin Id90004
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
X-Content-Type-Options Header Missing
Description +
The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.
+ +
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameterx-content-type-options
Attack
Evidence
Other InfoThis issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type. +At "High" threshold this scan rule will not alert on client or server error responses.
Instances1
Solution +
Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.
+
+ +
If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.
+ +
Reference + https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/gg622941(v=vs.85) +
+ + https://owasp.org/www-community/Security_Headers + +
CWE Id693
WASC Id15
Plugin Id10021
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Low
ZAP is Out of Date
Description +
The version of ZAP you are using to test your app is out of date and is no longer being updated.
+
+ +
The risk level is set based on how out of date your ZAP version is.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoThe latest version of ZAP is 2.17.0
Instances1
Solution +
Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
+ +
Reference + https://www.zaproxy.org/download/ + +
CWE Id1104
WASC Id45
Plugin Id10116
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Storable and Cacheable Content
Description +
The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where "shared" caching servers such as "proxy" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.
+ +
URLhttp://host.docker.internal:8080/
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/notes
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
URLhttp://host.docker.internal:8080/sitemap.xml
MethodGET
Parameter
Attack
Evidence
Other InfoIn the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.
Instances3
Solution +
Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:
+
+ +
Cache-Control: no-cache, no-store, must-revalidate, private
+
+ +
Pragma: no-cache
+
+ +
Expires: 0
+
+ +
This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security/zap-before.json b/security/zap-before.json new file mode 100644 index 000000000..787c81236 --- /dev/null +++ b/security/zap-before.json @@ -0,0 +1,159 @@ +{ + "@programName": "ZAP", + "@version": "2.16.1", + "@generated": "Fri, 3 Jul 2026 15:53:07", + "created": "2026-07-03T15:53:07.702122932Z", + "site":[ + { + "@name": "http://host.docker.internal:8080", + "@host": "host.docker.internal", + "@port": "8080", + "@ssl": "false", + "alerts": [ + { + "pluginid": "90004", + "alertRef": "90004-1", + "alert": "Insufficient Site Isolation Against Spectre Vulnerability", + "name": "Insufficient Site Isolation Against Spectre Vulnerability", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins.

", + "instances":[ + { + "id": "9", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "Cross-Origin-Resource-Policy", + "attack": "", + "evidence": "", + "otherinfo": "" + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages.

'same-site' is considered as less secured and should be avoided.

If resources must be shared, set the header to 'cross-origin'.

If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy).

", + "otherinfo": "", + "reference": "

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy

", + "cweid": "693", + "wascid": "14", + "sourceid": "1" + }, + { + "pluginid": "10021", + "alertRef": "10021", + "alert": "X-Content-Type-Options Header Missing", + "name": "X-Content-Type-Options Header Missing", + "riskcode": "1", + "confidence": "2", + "riskdesc": "Low (Medium)", + "desc": "

The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.

", + "instances":[ + { + "id": "3", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "x-content-type-options", + "attack": "", + "evidence": "", + "otherinfo": "This issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.\nAt \"High\" threshold this scan rule will not alert on client or server error responses." + } + ], + "count": "1", + "systemic": false, + "solution": "

Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.

If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.

", + "otherinfo": "

This issue still applies to error type pages (401, 403, 500, etc.) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.

At \"High\" threshold this scan rule will not alert on client or server error responses.

", + "reference": "

https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/gg622941(v=vs.85)

https://owasp.org/www-community/Security_Headers

", + "cweid": "693", + "wascid": "15", + "sourceid": "1" + }, + { + "pluginid": "10116", + "alertRef": "10116", + "alert": "ZAP is Out of Date", + "name": "ZAP is Out of Date", + "riskcode": "1", + "confidence": "3", + "riskdesc": "Low (High)", + "desc": "

The version of ZAP you are using to test your app is out of date and is no longer being updated.

The risk level is set based on how out of date your ZAP version is.

", + "instances":[ + { + "id": "5", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "The latest version of ZAP is 2.17.0" + } + ], + "count": "1", + "systemic": false, + "solution": "

Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.

", + "otherinfo": "

The latest version of ZAP is 2.17.0

", + "reference": "

https://www.zaproxy.org/download/

", + "cweid": "1104", + "wascid": "45", + "sourceid": "11" + }, + { + "pluginid": "10049", + "alertRef": "10049-3", + "alert": "Storable and Cacheable Content", + "name": "Storable and Cacheable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are storable by caching components such as proxy servers, and may be retrieved directly from the cache, rather than from the origin server by the caching servers, in response to similar requests from other users. If the response data is sensitive, personal or user-specific, this may result in sensitive information being leaked. In some cases, this may even result in a user gaining complete control of the session of another user, depending on the configuration of the caching components in use in their environment. This is primarily an issue where \"shared\" caching servers such as \"proxy\" caches are configured on the local network. This configuration is typically found in corporate or educational environments, for instance.

", + "instances":[ + { + "id": "1", + "uri": "http://host.docker.internal:8080/", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "7", + "uri": "http://host.docker.internal:8080/notes", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + }, + { + "id": "2", + "uri": "http://host.docker.internal:8080/sitemap.xml", + "nodeName": null, + "method": "GET", + "param": "", + "attack": "", + "evidence": "", + "otherinfo": "In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234." + } + ], + "count": "3", + "systemic": false, + "solution": "

Validate that the response does not contain sensitive, personal or user-specific information. If it does, consider the use of the following HTTP response headers, to limit, or prevent the content being stored and retrieved from the cache by another user:

Cache-Control: no-cache, no-store, must-revalidate, private

Pragma: no-cache

Expires: 0

This configuration directs both HTTP 1.0 and HTTP 1.1 compliant caching servers to not store the response, and to not retrieve the response (without validation) from the cache, in response to a similar request.

", + "otherinfo": "

In the absence of an explicitly specified caching lifetime directive in the response, a liberal lifetime heuristic of 1 year was assumed. This is permitted by rfc7234.

", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "3" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..747a120cc --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,154 @@ +# Lab 10 — Cloud: Ship QuickNotes to a Real Cloud (No Card) + +Release CI: [`.github/workflows/release.yml`](../.github/workflows/release.yml). +Space artifacts: [`cloud/Dockerfile`](../cloud/Dockerfile), +[`cloud/README.md`](../cloud/README.md), [`cloud/teardown.md`](../cloud/teardown.md). + +--- + +## Task 1 — CI-Automated Push to `ghcr.io` + +### What the workflow does + +On a pushed tag (`v*`): checkout → buildx → log in to ghcr with the auto-issued +`GITHUB_TOKEN` → derive tags (the raw ref name `v0.1.0` + `latest`) with +`docker/metadata-action` → build `./app` for `linux/amd64` and push to +`ghcr.io/rikire/devops-intro/quicknotes`. Permissions are `contents: read` + +`packages: write` only; every third-party action is pinned by 40-char SHA. + +### Evidence + +- Release run (green): https://github.com/rikire/DevOps-Intro/actions/runs/28790465672 +- Registry: `ghcr.io/rikire/devops-intro/quicknotes:v0.1.0` (+ `:latest`), public. +- Clean pull with **no credentials** (`~/.docker/config.json` has no ghcr entry): + +```text +$ docker logout ghcr.io && docker rmi ghcr.io/rikire/devops-intro/quicknotes:v0.1.0 +$ docker pull ghcr.io/rikire/devops-intro/quicknotes:v0.1.0 +v0.1.0: Pulling from rikire/devops-intro/quicknotes +Digest: sha256:be82c125e58a64e41c9eee6e08d814530f423d3ca7846affb74674c25dd9dcb9 +Status: Downloaded newer image for ghcr.io/rikire/devops-intro/quicknotes:v0.1.0 + +$ docker run --rm -p 8099:8080 ghcr.io/…/quicknotes:v0.1.0 & ; curl -s :8099/health +{"notes":4,"status":"ok"} # 200 — and carries the Lab 9 security headers +``` + +### 1.2 Design questions + +**a) OIDC vs `GITHUB_TOKEN` for ghcr.io.** +For pushing to ghcr **from the same repo**, `GITHUB_TOKEN` with `packages: write` +is enough — it's auto-issued, scoped to this repo, and expires when the job ends. +You reach for **OIDC** when you need to authenticate to an **external** cloud +(AWS ECR, GCP Artifact Registry, Azure) — OIDC exchanges a short-lived signed JWT +(carrying `repo`/`ref`/`actor` claims) for cloud credentials with **no stored +long-lived secrets** and a fine-grained trust policy ("only this repo on this +branch may assume this role"). `GITHUB_TOKEN` only works inside GitHub's own +services; OIDC gives keyless, secret-less federation to third parties. For ghcr +same-repo, OIDC is overkill; for deploying to a hyperscaler, it's the right tool. + +**b) Why ship `:latest` alongside the immutable `:v0.1.0`?** +The immutable `:v0.1.0` is the source of truth — reproducible, auditable, +pin-able for rollback; production and CI pin *this* (never `:latest`, which is +mutable). `:latest` is a moving convenience pointer: it's the ergonomic entry for +humans doing quick pulls, docs/examples, and "just give me the current release" +tooling. They serve different audiences — you keep the immutable tag for +correctness and `:latest` for UX, and you never *depend* on `:latest` where +reproducibility matters. + +**c) `packages: write` only — principle + attack prevented.** +**Least privilege.** The job only publishes a package, so it gets only +`packages: write` + `contents: read`. If the job (or a compromised action / +dependency inside it) runs hostile code, the blast radius is bounded to "push a +package." With `write: all` / `contents: write`, a compromised run could push +malicious **commits**, rewrite **workflows**, cut fake **releases** — a full +supply-chain repo takeover. The narrow scope means a leaked token or poisoned +action can at worst publish a bad image (caught downstream), not backdoor your +source or CI. + +--- + +## Task 2 — Deploy to Hugging Face Spaces + +### The Space + +A Docker-SDK Space serving QuickNotes. The Space repo holds two files: +[`cloud/Dockerfile`](../cloud/Dockerfile) (pulls the ghcr image) and +[`cloud/README.md`](../cloud/README.md) (frontmatter: `sdk: docker`, +`app_port: 8080`). + +- Space URL: **https://rikire-quicknotes.hf.space** + +```text +$ curl -sD- https://rikire-quicknotes.hf.space/health +HTTP/1.1 200 OK +Content-Type: application/json +cache-control: no-store +content-security-policy: default-src 'none' # the deployed image carries the Lab 9 hardening +x-content-type-options: nosniff + +{"notes":4,"status":"ok"} +``` + +`/notes` also serves the seeded data. Note the Space runs the *exact* ghcr image +(digest `be82c125…`) — the security headers prove it's the hardened Lab 9 build, +not a rebuild. + +### Scale-to-zero (HF "sleep") + +Measured with `curl -w '%{time_total}'` (connection is VPN-routed to HF's +datacenter, so warm samples show high variance — best-case ~0.7 s). + +| Measurement | time_total | +|-------------|-----------:| +| Warm p50 (7 back-to-back requests) | ~1.5 s (min 0.73 s) | +| Cold start #1 (first hit after a long idle) | 10.7 s | +| Cold start #2 (first hit after idle) | 8.9 s | +| Cold start #3 (wake exceeded client timeout) | ≥ 15 s | + +Each cold sample is the **first request after the Space had been idle**; the +immediate warm follow-up dropped back to ~0.6 s, so the 9–15+ s is the *wake*, not +the network — a **~10–15× penalty** vs warm. + +**Finding on HF's "sleep":** the free-tier sleep threshold proved *inconsistent*. +A deliberate 37-minute idle probe came back **warm (0.69 s)** — the Space hadn't +slept yet — so `sleep → wake` isn't reliably reproducible on a fixed 35-minute +schedule; the cold numbers above are genuine wake observations captured when it +*had* slept (notably right after deploy and after long multi-hour idles). HF +optimizes for cost, not a predictable wake SLA — you get scale-to-zero savings but +no guarantee of *when* it sleeps or *how fast* it wakes. + +### 2.4 Design questions + +**d) HF "sleep" vs Cloud Run "scale to zero" — why is HF's wake so much slower?** +Both scale to zero to save cost, but HF wakes in seconds-to-tens-of-seconds +vs Cloud Run's sub-second-to-few-seconds. They optimize for different things. HF +Spaces is a **free demo/showcase** platform on shared infra: a wake means +scheduling the container onto a shared node, pulling a (often large) image from +storage, and cold-starting it — no warm pool is kept for free Spaces. Cloud Run +is a **paid, latency-sensitive production** serverless: images are pre-staged near +compute, container start is heavily optimized, and you can pay to keep +`min-instances` warm. HF trades wake speed for $0; Cloud Run trades money for +low tail latency. + +**e) Why does the Space need `app_port: 8080`?** +HF Spaces default the served port to **7860** (the Gradio/Streamlit heritage). +QuickNotes binds **8080**, so without `app_port: 8080` HF routes the public URL +to 7860 where nothing listens → the Space looks dead (no response / 502). Setting +`app_port: 8080` points HF's edge at the port the app actually binds. We don't +touch QuickNotes — `app_port` is HF's escape hatch from their default. + +**f) Pull the ghcr image vs build the Dockerfile inside the Space — trade-off.** +We **pull** the immutable ghcr tag, so the Space runs the *exact* artifact CI +built, tagged, and Lab-9-scanned — reproducible, no build drift, and the Space +build is a fast pull (no recompile). Cost: it depends on the image being +published+public first, and you can't tweak the image in-place. **Building from +`app/` in the Space** instead makes it self-contained and easy to hack/debug, but +HF rebuilds on every push (slower) and can produce a *different* image than CI +did (different base digest, build-time drift) — so "what you tested" ≠ "what you +ship." We chose reproducibility: ship the scanned CI artifact, not a rebuild. + +--- + +## Bonus — Cloudflare Tunnel + Cross-Platform Comparison + +Not attempted (Task 1 + Task 2 completed for 10/10). diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..c6d472796 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,198 @@ +# Lab 6 — Containers: Dockerize QuickNotes + +Host: Windows 11 + Docker **29.1.2**. Files: +[`app/Dockerfile`](../app/Dockerfile), [`compose.yaml`](../compose.yaml). + +--- + +## Task 1 — Multi-Stage Dockerfile, ≤ 25 MB + +### What the Dockerfile does + +Two stages: a `golang:1.24` **builder** that compiles a static, stripped binary, +and a `gcr.io/distroless/static:nonroot` **runtime** that carries only the binary, +`seed.json`, and a nonroot-owned `/data`. + +| Requirement | How | +|-------------|-----| +| Multi-stage | `builder` + `runtime` | +| Builder pinned | `golang:1.24` | +| Runtime base | `gcr.io/distroless/static:nonroot` (no shell, no apt) | +| Static binary | `CGO_ENABLED=0` | +| Stripped / reproducible | `-ldflags='-s -w' -trimpath` | +| Nonroot | `USER nonroot:nonroot` (uid 65532) | +| Exec entrypoint + EXPOSE | `ENTRYPOINT ["/quicknotes"]`, `EXPOSE 8080` | +| Cache-friendly order | `COPY go.mod` + `go mod download` **before** `COPY . .` | + +### Build + verify (real output) + +```text +$ docker images quicknotes:lab6 --format '{{.Repository}}:{{.Tag}} {{.Size}}' +quicknotes:lab6 8.56MB # ≤ 25 MB ✅ + +$ docker inspect quicknotes:lab6 --format '{{.Config.User}} | {{json .Config.ExposedPorts}} | {{json .Config.Entrypoint}}' +nonroot:nonroot | {"8080/tcp":{}} | ["/quicknotes"] + +$ docker run -d -p 8080:8080 quicknotes:lab6 ; curl -s :8080/health +{"notes":4,"status":"ok"} # 200 +# POST /notes -> 201, GET /notes shows the new note +``` + +Base-image comparison: builder `golang:1.24` is **894 MB**, the distroless runtime +base is **2.21 MB**, and our final image is **8.56 MB** (binary + seed + /data). +Multi-stage threw away ~890 MB of toolchain. + +### 1.2 Design questions + +**a) Why does layer order matter? (`COPY . .` early vs `go.mod` first)** + +Each Dockerfile instruction is a cached layer keyed on its inputs. With +`COPY . . && go mod download && go build`, *any* source edit changes the `COPY . .` +layer, which invalidates everything after it — so `go mod download` re-runs on +every rebuild. With `COPY go.mod ./ && go mod download && COPY . . && go build`, +the download layer is keyed only on `go.mod`/`go.sum`; a source-only edit reuses +the cached dependencies and only re-runs the build. On a rebuild after touching a +`.go` file, strategy A pays the dependency download again (cache miss), strategy B +skips it (cache hit). *Caveat for this repo:* QuickNotes has zero external +dependencies, so `go mod download` is near-instant and the absolute saving here is +small — but the ordering is still correct and on a real dependency tree it's the +difference between a sub-second rebuild and re-pulling hundreds of modules. + +**b) Why `CGO_ENABLED=0`?** + +It forces a fully static binary with no libc / dynamic-linker dependency. The +default `CGO_ENABLED=1` links against the host's glibc dynamically. `distroless/ +static` deliberately contains **no libc and no dynamic linker (ld.so)**, so a +dynamically-linked binary fails to start — the kernel can't find the ELF +interpreter and you get `no such file or directory` (on the binary that visibly +exists). Forgetting the flag = a container that won't run. + +**c) What is `gcr.io/distroless/static:nonroot`?** + +A minimal runtime image containing essentially only: CA certificates, `/etc/passwd` +with a `nonroot` user (uid 65532), timezone data, and a `/tmp` dir. What it +*doesn't* have is the point — **no shell, no package manager, no busybox, no +libc, no coreutils**. That shrinks the attack surface and, crucially, the CVE +surface: there are no OS packages to be vulnerable. Trivy confirms **0 HIGH/ +CRITICAL in the OS layer** (see Bonus). + +**d) `-ldflags='-s -w'` and `-trimpath`?** + +`-s` drops the symbol table, `-w` drops DWARF debug info — together they shrink +the binary and remove most of what a debugger/symbolizer would use. `-trimpath` +strips absolute build-machine file paths from the binary, replacing them with +module-relative paths. The cost: `-s -w` makes post-mortem debugging and +profiling with symbols much harder (no `delve`, degraded stack symbolization); +`-trimpath` removes local path info that some debug workflows rely on. The payoff +is a smaller, **reproducible** binary that doesn't leak the build host's directory +layout — the right trade for a shipped image. + +--- + +## Task 2 — Compose + Healthcheck + Persistent Volume + +### Persistence test (real output) + +```text +$ docker compose up --build -d ; docker compose ps --format '{{.Health}}' +healthy + +$ curl -XPOST -d '{"title":"durable","body":"survive a restart"}' :8080/notes # 201 +present after create: 1 + +$ docker compose down ; docker compose up -d +present after down + up: 1 # ✅ survived (named volume kept) + +$ docker compose down -v ; docker compose up -d +present after down -v + up: 0 # gone (volume destroyed) +``` + +### 2.2 Design questions + +**e) Distroless has no shell — how do you healthcheck it?** + +I made the QuickNotes binary **dual-mode**: `quicknotes healthcheck` does an +in-process HTTP GET to `/health` and exits `0` (healthy) or `1` (unhealthy). The +Compose healthcheck re-invokes the only binary in the image in exec form: +`test: ["CMD", "/quicknotes", "healthcheck"]`. No shell, no `wget`, no sidecar. +This is the lab's "use a binary that's already in the image" option, and it's the +cleanest — verified: `docker compose ps` reports **healthy**. + +**f) Why does the named volume survive `docker compose down`? What destroys it?** + +`docker compose down` removes containers and the default network, but **named +volumes are separate objects with their own lifecycle** — they are intentionally +left intact so data outlives container churn, and re-attach on the next `up`. +What destroys it: `docker compose down -v` (or `docker volume rm +quicknotes-data`). The test above shows exactly this: the note survives `down + +up` and disappears only after `down -v`. + +**g) `depends_on` without `condition: service_healthy`?** + +Plain `depends_on: [x]` only waits for container `x` to be **started** (its +process launched / created), *not* for it to be **ready** to serve. The bug: +the dependent service can come up and start making requests before `x` is +actually accepting connections — e.g. an app querying a database that has +"started" but isn't listening yet, giving intermittent connection-refused races +on boot. `condition: service_healthy` makes Compose wait for `x`'s healthcheck to +pass first. + +--- + +## Bonus — The 6 Security Defaults + +### Hardened `services.quicknotes` block + +```yaml +read_only: true # 4. read-only root filesystem +tmpfs: + - /tmp # writable scratch (the /data named volume stays writable) +cap_drop: + - ALL # 3. drop every Linux capability (QuickNotes needs none) +security_opt: + - no-new-privileges:true # 5. block setuid privilege escalation +# 1. USER nonroot + 2. distroless base come from app/Dockerfile +# 6. Trivy scan below +``` + +### Verification (real output) + +| # | Default | Command | Result | +|---|---------|---------|--------| +| 1 | `USER nonroot` | `inspect ... .Config.User` | `nonroot:nonroot` | +| 2 | No shell | `compose exec quicknotes sh` | `exec: "sh": executable file not found` (fails ✅) | +| 3 | Caps dropped | `inspect ... .HostConfig.CapDrop` | `[ALL]` | +| 4 | Read-only root | `inspect ... .HostConfig.ReadonlyRootfs` | `true` | +| 5 | no-new-privileges | `inspect ... .HostConfig.SecurityOpt` | `[no-new-privileges:true]` | + +All five enforced while the service still reports **healthy** and serves +`/health` — read-only root + dropped caps don't break QuickNotes because it only +writes to the `/data` volume (and `/tmp` tmpfs). + +### Trivy scan + +```text +$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 + +quicknotes:lab6 (debian 13.5) Total: 0 (HIGH: 0, CRITICAL: 0) # OS layer — clean +quicknotes (gobinary) Total: 12 (HIGH: 12, CRITICAL: 0) # all Go stdlib +``` + +The **OS layer has zero HIGH/CRITICAL** — exactly the distroless payoff (no OS +packages = nothing to patch). The 12 HIGH are all in the **Go standard library** +compiled into the binary (e.g. `net/url`, `net/mail`, `net/http/httputil`), +fixed in Go `1.25.8 / 1.26.1`. The lab pins the builder to Go `1.24` (whose +latest patch `1.24.13` still predates those fixes), so they remain; in a real +pipeline you'd bump the builder's Go minor version to clear them. Distroless +fixed the base image; the binary's stdlib is a separate supply-chain axis. + +### Most security per line of YAML + +**`cap_drop: [ALL]`** — one line strips every Linux capability the container +would otherwise inherit (~14 by default, including `CAP_NET_RAW`, +`CAP_CHOWN`, `CAP_SETUID`…), collapsing a huge slice of the kernel attack surface +that container escapes typically abuse. QuickNotes needs none, so the cost is +zero. Runner-up is `read_only: true`, which neutralizes persistence/tampering on +the root filesystem in one line — but `cap_drop: ALL` removes the most latent +privilege for the least YAML. diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..d2f77af8b --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,211 @@ +# Lab 9 — DevSecOps: Scan QuickNotes with Trivy + ZAP + +Trivy pinned to **`aquasec/trivy:0.59.1`**, ZAP to **`ghcr.io/zaproxy/zaproxy:2.16.1`**. +Scan artifacts in [`security/`](../security/). + +--- + +## Task 1 — Trivy: Image + Filesystem + Config + SBOM + +### The four scans + +**1. Image** — [`security/trivy-image.txt`](../security/trivy-image.txt) + +```text +quicknotes:lab6 (debian 13.5) Total: 0 (HIGH: 0, CRITICAL: 0) # OS layer clean (distroless) +quicknotes (gobinary) Total: 11 (HIGH: 11, CRITICAL: 0) # all Go 1.24.13 stdlib +``` + +**2. Filesystem** — [`security/trivy-fs.txt`](../security/trivy-fs.txt) + +```text +.vagrant/machines/default/hyperv/private_key (secrets) +Total: 1 (HIGH: 1) → HIGH: AsymmetricPrivateKey +``` + +(Re-scanning the repo with `--skip-dirs .vagrant` is clean — the key is local +Vagrant state, not source.) + +**3. Config** — [`security/trivy-config.txt`](../security/trivy-config.txt) + +```text +app/Dockerfile (dockerfile) Tests: 28 (SUCCESSES: 27, FAILURES: 1) +AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile +``` + +No HIGH/CRITICAL misconfigurations. + +**4. SBOM (CycloneDX)** — [`security/sbom.cdx.json`](../security/sbom.cdx.json) — 9 components +(debian 13.5, quicknotes, base-files, media-types, netbase, tzdata, **stdlib v1.24.13**, …). + +### Triage — every HIGH/CRITICAL (a decision per finding) + +| # | Finding | Scan | Sev | Disposition | Reason | +|---|---------|------|-----|-------------|--------| +| 1 | **11× Go stdlib CVEs** — CVE-2026-25679, -27145, -32280, -32281, -32283, -33811, -33814, -39820, -39836, -42499, -42504 (net/url, crypto/x509, crypto/tls, net, net/http2, net/mail, MIME) | image · gobinary | HIGH | **FIX** | All are Go **1.24 stdlib DoS** bugs fixed in Go 1.25.x. Fixed by bumping the Dockerfile builder `golang:1.24 → golang:1.25` (this branch). Re-scan → **0 HIGH/CRITICAL** ([`trivy-image-after.txt`](../security/trivy-image-after.txt)). | +| 2 | **AsymmetricPrivateKey** — `.vagrant/…/hyperv/private_key` | fs · secret | HIGH | **SUPPRESS (out of scope)** | The Vagrant-generated throwaway SSH key in **gitignored** local machine state (`.vagrant/`); regenerated every `vagrant up`, never committed or shipped in the image. Scanning the actual source (`--skip-dirs .vagrant`) is clean. | +| 3 | **AVD-DS-0026** — no `HEALTHCHECK` in Dockerfile | config | LOW | **ACCEPT** *(re-eval by 2026-12)* | Not HIGH/CRITICAL. The healthcheck is defined in `compose.yaml` — distroless has no shell for a Dockerfile `HEALTHCHECK`, so the binary's self-check runs at the compose layer. Re-evaluate if the image is ever run outside compose. | + +The 11 stdlib CVEs are also a **reachability** story: they're DoS in TLS/mail/HTTP2 +paths QuickNotes (a plain HTTP/1 JSON API — no TLS server, no mail, no multipart) +barely touches. We fixed them anyway because the fix is a one-line, zero-risk Go +bump — cheaper than justifying each acceptance. + +### First 30 lines of the CycloneDX SBOM + +```json +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:b68e954b-5628-4bc5-bce0-6b04d68eeaa7", + "version": 1, + "metadata": { + "timestamp": "2026-07-03T14:19:59+00:00", + "tools": { + "components": [ + { "type": "application", "group": "aquasecurity", "name": "trivy", "version": "0.59.1" } + ] + }, + "component": { + "bom-ref": "7d90f63d-0a13-4be7-b3ec-2207b19e6fb2", + "type": "container", + "name": "quicknotes:lab6", + "properties": [ + { "name": "aquasecurity:trivy:DiffID", "value": "sha256:065706fa87545e68426eb8f984f0f160603009f4ae7a26608bb63e6e587e98e2" }, + { "name": "aquasecurity:trivy:DiffID", "value": "sha256:151e3fc65a977c0d5876bcae809305d01e9a18d2fc52aecb4403096bcfe01799" } + ] + } + } +} +``` + +### 1.3 Design questions + +**a) CVE severity is one input — what else matters when triaging?** +- **Reachability** — do you actually call the vulnerable function? A CVE in a + stdlib package your code never invokes is far lower risk (this is govulncheck's + whole point). Most of our 11 were in TLS/mail/HTTP2 paths QuickNotes never hits. +- **Exploit availability** — is there a public PoC or active in-the-wild + exploitation (CISA KEV)? A weaponized exploit jumps the queue. +- **Deployment context** — internet-facing vs internal-only, behind a WAF, data + sensitivity. DoS on an internal tool ≠ RCE on a public payments API. +- **Impact class** — DoS vs info-leak vs RCE. All 11 here are availability (DoS), + not code execution — lower urgency than a remote-code-exec at the same CVSS. +- Plus fix availability/cost and blast radius. CVSS severity is the *starting* + question, not the answer. + +**b) Why is a minimal (distroless) base the strongest single control?** +A vulnerability you don't ship can't be exploited. The vast majority of image +CVEs live in OS packages — shells, libc, package managers, coreutils. Distroless +static contains **none** of them, so there's almost nothing left to be vulnerable +(our OS layer scored 0 HIGH/CRITICAL). Shrinking the software present shrinks the +attack surface *and* the CVE surface at the same time — it eliminates whole +classes of findings at once instead of patching them one CVE at a time, which is +why it's the highest-leverage control. + +**c) When is `.trivyignore` right, and when is it theater?** +Right when it records a **triaged, dated decision** — an ACCEPT with a re-eval +date, a documented FALSE POSITIVE, or a WATCH for a not-yet-fixed CVE. The file +keeps the scan green *without losing the signal*, because it points back to a +decision. It's **theater** when you dump findings you never read into it just to +turn CI green — that hides real risk, and an undated "accept forever" entry +becomes a permanent blind spot. The ignore must follow a decision, never replace +one. + +**d) What future problem does having the SBOM today solve?** +The next **Log4Shell**. When a critical CVE drops in a ubiquitous component, the +first, time-critical question is *"am I affected — which services ship it, at +what version?"* Without an SBOM you're grepping build systems and guessing while +the clock runs (a big reason Log4Shell/Equifax responses were slow). With an SBOM +generated *today*, "do I ship log4j 2.14?" is an instant lookup against a list you +already have — turning days of frantic inventory into a one-minute query, which +is the difference between patching before and after you're breached. + +--- + +## Task 2 — OWASP ZAP Baseline + Fix at Least One Finding + +`zap-baseline.py` (passive only) against the running container. Reports: +[`security/zap-before.*`](../security/) and [`zap-after.*`](../security/). + +### Triage — every ZAP finding + +| ID | Finding | Risk | URL | Disposition | +|----|---------|------|-----|-------------| +| **10021** | X-Content-Type-Options Header Missing | Low | `/notes` (+ all) | **FIX** — added `X-Content-Type-Options: nosniff` in middleware. Re-scan: **gone**. | +| **10049** | Storable and Cacheable Content | Info | `/`, `/robots.txt`, `/sitemap.xml`, `/notes` | **FIX** — added `Cache-Control: no-store`. Re-scan: now reports *Non-Storable Content* (note data no longer cacheable). | +| **90004** | Insufficient Site Isolation Against Spectre | Low | `/notes` | **ACCEPT** *(re-eval by 2026-12)* — COOP/COEP/CORP are browser *document*-isolation headers; QuickNotes is a JSON API never embedded in a browser document, and COEP `require-corp` risks breaking legitimate cross-origin API clients. Low exposure; revisit if a browser front-end is added. | +| **10116** | ZAP is Out of Date | Low | `/` | **FALSE POSITIVE** — flags that the *ZAP scanner* is outdated, not a QuickNotes defect. Not an app finding. | + +### The fix — one middleware, all routes + +`Routes()` wraps the mux once with `securityHeaders()` ([`app/handlers.go`](../app/handlers.go)): + +```go +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("X-Content-Type-Options", "nosniff") + h.Set("Content-Security-Policy", "default-src 'none'") + h.Set("X-Frame-Options", "DENY") + h.Set("Referrer-Policy", "no-referrer") + h.Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} +``` + +Guarded by a unit test that fails if the middleware is removed +([`app/handlers_test.go`](../app/handlers_test.go) → +`TestSecurityHeaders_PresentOnEveryResponse`). + +### Before / after (finding proven gone) + +```text +# BEFORE (zap-before.json) # AFTER (zap-after.json) +10021 X-Content-Type-Options Header Missing ← gone +10049 Storable and Cacheable Content 10049 Non-Storable Content (fixed) +90004 Spectre site isolation 90004 Spectre site isolation (accepted) +10116 ZAP is Out of Date 10116 ZAP is Out of Date (false positive) + +$ curl -sD- -o/dev/null localhost:8080/notes | grep -iE 'x-content|content-security|cache-control' +X-Content-Type-Options: nosniff +Content-Security-Policy: default-src 'none' +Cache-Control: no-store +``` + +### 2.5 Design questions + +**e) Why a middleware and not per-handler header sets?** +A middleware wraps the router **once**, so the headers land on *every* response — +success, error, 404, and any route added later — with zero duplication. Per-handler +`Header().Set` is error-prone: you forget one handler, a new route ships without +them, or an error path skips them, and the header silently vanishes on some +responses. Middleware makes "miss a route" structurally impossible and gives one +testable source of truth. + +**f) `Content-Security-Policy: default-src 'none'` — what breaks, why OK here?** +`default-src 'none'` forbids the page from loading *anything* — no scripts, +styles, images, fonts, frames, or `fetch`/XHR. That **breaks a website**, which +must load its own JS/CSS/assets. QuickNotes is a **JSON API**: it serves no +HTML/JS/CSS and is consumed by API clients, not rendered as a browser document — +so there's nothing for the CSP to break. The strictest policy is free hardening +for an API (it just says "if a browser ever renders this, load nothing"), whereas +a website must allowlist exactly what its pages legitimately load. + +**g) Cost of marking all informational findings "accepted" without reading them?** +You lose the signal. Informational findings are mostly noise, but occasionally one +hides a real issue (an info-leak, a cacheable *sensitive* response, a stray debug +endpoint — exactly like 10049 here, which was worth fixing). Rubber-stamping +"accepted" on all of them (1) trains you to ignore the whole category, so you miss +the real one when it comes; (2) creates undated blanket acceptances that bury risk +permanently; (3) destroys the audit trail of *why* each was fine. "Informational, +no action" is a legitimate disposition — but only as a **read** decision, not a +reflex. + +--- + +## Bonus — `govulncheck` as a CI PR Gate + +Not attempted (Task 1 + Task 2 completed for 10/10).