diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..4dbd6de08 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Goal + + +Complete lab 3 DevOps + +## Changes +* Added submissions/lab3.md +* Create GitHub Action CI Workflows +* Added Go environment ubuntu 24.04 + +## Testing + +* Start CI action on GitHub and green CI run +* Verfied CI run pipeline + +## Checklist +- ☑️ Title is a clear sentence (≤ 70 chars) +- ☑️ Vet, Test, and Lint checks pass +- ☑️ `submissions/lab3.md` updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..38dbdc45d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +name: PR Gate + +on: + push: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read # минимальные права + +jobs: + # ---------- vet ---------- + vet: + name: go vet + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + defaults: + run: + working-directory: app + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + - name: Run go vet + run: go vet ./... + + # ---------- test ---------- + test: + name: go test -race + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + defaults: + run: + working-directory: app + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + - name: Run tests with race detector + run: go test -race -count=1 ./... + + # ---------- lint ---------- + lint: + name: golangci-lint + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: app + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.3.0 + with: + go-version: '1.24' # достаточно одной версии для линтера + cache: true + - name: Install golangci-lint v2.5.0 + run: | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ + sh -s -- -b $(go env GOPATH)/bin v2.5.0 + - name: Run golangci-lint + run: golangci-lint run ./... \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c70becb40 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: Release to GitHub Container Registry + +on: + push: + tags: + - 'v*' # Triggers on tags like v0.1.0, v1.2.3, etc. + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/quicknotes + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # Minimum required scope for pushing to ghcr.io + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@369eb591f10d871d5d1e7e7e6a5d7a3bc63f3b4c # v5.6.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + with: + context: ./app + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/ansible/files/quicknotes b/ansible/files/quicknotes new file mode 100644 index 000000000..cfa09c3cc Binary files /dev/null and b/ansible/files/quicknotes differ diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 000000000..6941b721c --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,2 @@ +[quicknotes_vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_private_key_file=~/.vagrant.d/insecure_private_key ansible_ssh_extra_args="-o StrictHostKeyChecking=no" \ No newline at end of file diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml new file mode 100644 index 000000000..d441a8914 --- /dev/null +++ b/ansible/playbook.yaml @@ -0,0 +1,65 @@ +--- +- name: Deploy QuickNotes to Lab 5 VM + hosts: quicknotes_vm + become: true + gather_facts: true + + vars: + app_user: quicknotes + app_data_dir: /var/lib/quicknotes + app_binary_path: /usr/local/bin/quicknotes + app_listen_addr: ":9090" + app_data_path: "{{ app_data_dir }}/data.db" + app_seed_path: "{{ app_data_dir }}/seed.json" + + tasks: + - name: Create quicknotes system user + ansible.builtin.user: + name: "{{ app_user }}" + system: true + shell: /usr/sbin/nologin + home: "{{ app_data_dir }}" + create_home: false + + - name: Ensure data directory exists + ansible.builtin.file: + path: "{{ app_data_dir }}" + state: directory + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: '0750' + + - name: Copy QuickNotes binary + ansible.builtin.copy: + src: files/quicknotes + dest: "{{ app_binary_path }}" + mode: '0755' + owner: root + group: root + notify: restart quicknotes + + - name: Render systemd unit file + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: /etc/systemd/system/quicknotes.service + owner: root + group: root + mode: '0644' + notify: restart quicknotes + + - name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + + - name: Enable and start QuickNotes service + ansible.builtin.systemd: + name: quicknotes + enabled: true + state: started + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: quicknotes + state: restarted + daemon_reload: true \ No newline at end of file diff --git a/ansible/templates/quicknotes.service.j2 b/ansible/templates/quicknotes.service.j2 new file mode 100644 index 000000000..875c00d42 --- /dev/null +++ b/ansible/templates/quicknotes.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=QuickNotes Service +After=network-online.target +Wants=network-online.target + +[Service] +User={{ app_user }} +Group={{ app_user }} +WorkingDirectory={{ app_data_dir }} +ExecStart={{ app_binary_path }} +Restart=on-failure +RestartSec=5 +Environment="ADDR={{ app_listen_addr }}" +Environment="DATA_PATH={{ app_data_path }}" +Environment="SEED_PATH={{ app_seed_path }}" + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..67abe2703 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,40 @@ +#---------- Builder stage ---------- + +FROM golang:1.24-alpine AS builder + +# WorkDir + +WORKDIR /build + +# copy go.mod and run + +COPY go.mod ./ +RUN go mod download + +COPY . . + + +# CGO_ENABLED=0 — disable CGO +# -ldflags='-s -w' — delete table symbol + +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# -------------------- Runtime stage -------------------- +FROM gcr.io/distroless/static:nonroot + +# copy bin from builder-stage + +COPY --from=builder /build/quicknotes /quicknotes + +# UID 65532 in distroless/static:nonroot + +USER 65532:65532 + +# port +EXPOSE 8080 + +# exec-form +ENTRYPOINT ["/quicknotes"] \ No newline at end of file diff --git a/app/lab4-trace.pcap b/app/lab4-trace.pcap new file mode 100644 index 000000000..a72a4046f Binary files /dev/null and b/app/lab4-trace.pcap differ diff --git a/app/newfile.txt b/app/newfile.txt new file mode 100644 index 000000000..38cec2660 --- /dev/null +++ b/app/newfile.txt @@ -0,0 +1 @@ +dsd diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..b8b2fc71d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,61 @@ +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + container_name: quicknotes + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json + healthcheck: + test: ["CMD", "/quicknotes", "-healthcheck"] # or ["CMD-SHELL", "wget -qO- http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + restart: unless-stopped + + prometheus: + image: prom/prometheus:v3.0.1 + container_name: prometheus + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + depends_on: + quicknotes: + condition: service_healthy + networks: + - quicknotes-net + + grafana: + image: grafana/grafana:13.0.0 + container_name: grafana + volumes: + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/provisioning/dashboards/golden-signals.json:/var/lib/grafana/dashboards/golden-signals.json:ro + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=securepassword123 + - GF_INSTALL_PLUGINS= + depends_on: + - prometheus + networks: + - quicknotes-net + +networks: + quicknotes-net: + driver: bridge + + +volumes: + quicknotes-data: + name: quicknotes-data \ No newline at end of file diff --git a/gen.conf b/gen.conf new file mode 100644 index 000000000..f668d824d --- /dev/null +++ b/gen.conf @@ -0,0 +1,70 @@ +# zap-baseline rule configuration file +# Change WARN to IGNORE to ignore rule or FAIL to fail if rule matches +# Only the rule identifiers are used - the names are just for info +# You can add your own messages to each rule by appending them after a tab on each line. +10003 WARN (Vulnerable JS Library (Powered by Retire.js)) +10009 WARN (In Page Banner Information Leak) +10010 WARN (Cookie No HttpOnly Flag) +10011 WARN (Cookie Without Secure Flag) +10015 WARN (Re-examine Cache-control Directives) +10017 WARN (Cross-Domain JavaScript Source File Inclusion) +10019 WARN (Content-Type Header Missing) +10020 WARN (Anti-clickjacking Header) +10021 WARN (X-Content-Type-Options Header Missing) +10023 WARN (Information Disclosure - Debug Error Messages) +10024 WARN (Information Disclosure - Sensitive Information in URL) +10025 WARN (Information Disclosure - Sensitive Information in HTTP Referrer Header) +10026 WARN (HTTP Parameter Override) +10027 WARN (Information Disclosure - Suspicious Comments) +10028 WARN (Off-site Redirect) +10029 WARN (Cookie Poisoning) +10030 WARN (User Controllable Charset) +10031 WARN (User Controllable HTML Element Attribute (Potential XSS)) +10032 WARN (Viewstate) +10033 WARN (Directory Browsing) +10034 WARN (Heartbleed OpenSSL Vulnerability (Indicative)) +10035 WARN (Strict-Transport-Security Header) +10036 WARN (HTTP Server Response Header) +10037 WARN (Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)) +10038 WARN (Content Security Policy (CSP) Header Not Set) +10039 WARN (X-Backend-Server Header Information Leak) +10040 WARN (Secure Pages Include Mixed Content) +10041 WARN (HTTP to HTTPS Insecure Transition in Form Post) +10042 WARN (HTTPS to HTTP Insecure Transition in Form Post) +10043 WARN (User Controllable JavaScript Event (XSS)) +10044 WARN (Big Redirect Detected (Potential Sensitive Information Leak)) +10049 WARN (Content Cacheability) +10050 WARN (Retrieved from Cache) +10052 WARN (X-ChromeLogger-Data (XCOLD) Header Information Leak) +10054 WARN (Cookie without SameSite Attribute) +10055 WARN (CSP) +10056 WARN (X-Debug-Token Information Leak) +10057 WARN (Username Hash Found) +10061 WARN (X-AspNet-Version Response Header) +10062 WARN (PII Disclosure) +10063 WARN (Permissions Policy Header Not Set) +10096 WARN (Timestamp Disclosure) +10097 WARN (Hash Disclosure) +10098 WARN (Cross-Domain Misconfiguration) +10099 WARN (Source Code Disclosure) +10105 WARN (Weak Authentication Method) +10108 WARN (Reverse Tabnabbing) +10109 WARN (Modern Web Application) +10110 WARN (Dangerous JS Functions) +10111 WARN (Authentication Request Identified) +10112 WARN (Session Management Response Identified) +10113 WARN (Verification Request Identified) +10115 WARN (Script Served From Malicious Domain (polyfill)) +10116 WARN (ZAP is Out of Date) +10202 WARN (Absence of Anti-CSRF Tokens) +2 WARN (Private IP Disclosure) +3 WARN (Session ID in URL Rewrite) +50001 WARN (Script Passive Scan Rules) +90001 WARN (Insecure JSF ViewState) +90002 WARN (Java Serialization Object) +90003 WARN (Sub Resource Integrity Attribute Missing) +90004 WARN (Insufficient Site Isolation Against Spectre Vulnerability) +90011 WARN (Charset Mismatch) +90022 WARN (Application Error Disclosure) +90030 WARN (WSDL File Detection) +90033 WARN (Loosely Scoped Cookie) diff --git a/lab4-trace.pcap b/lab4-trace.pcap new file mode 100644 index 000000000..90be702b5 Binary files /dev/null and b/lab4-trace.pcap differ diff --git a/lab4-trace.txt b/lab4-trace.txt new file mode 100644 index 000000000..a6299ac48 --- /dev/null +++ b/lab4-trace.txt @@ -0,0 +1,132 @@ +19:11:19.932815 IP 127.0.0.1.42566 > 127.0.0.1.8080: Flags [S], seq 3179416048, win 65495, options [mss 65495,sackOK,TS val 2461922435 ecr 0,nop,wscale 7], length 0 +E..<..@.@.(..........F.... ..........0......... +............ +19:11:19.932821 IP 127.0.0.1.8080 > 127.0.0.1.42566: Flags [R.], seq 0, ack 3179416049, win 0, length 0 +E..(..@.@.<............F...... .P...$... +19:11:19.932861 IP6 ::1.40002 > ::1.8080: Flags [S], seq 2374255427, win 65476, options [mss 65476,sackOK,TS val 2676338689 ecr 0,nop,wscale 7], length 0 +`.~..(.@.................................B....CC.........0......... +............ +19:11:19.932866 IP6 ::1.8080 > ::1.40002: Flags [R.], seq 0, ack 2374255428, win 0, length 0 +`.k9...@...................................B......CDP....... +19:12:26.641138 IP 127.0.0.1.41710 > 127.0.0.1.8080: Flags [S], seq 509650619, win 65495, options [mss 65495,sackOK,TS val 2461989143 ecr 0,nop,wscale 7], length 0 +E..<1.@.@. +..............`...........0......... +............ +19:12:26.641142 IP 127.0.0.1.8080 > 127.0.0.1.41710: Flags [R.], seq 0, ack 509650620, win 0, length 0 +E..(..@.@.<..................`..P...*3.. +19:12:26.641165 IP6 ::1.32816 > ::1.8080: Flags [S], seq 630744336, win 65476, options [mss 65476,sackOK,TS val 2676405397 ecr 0,nop,wscale 7], length 0 +` .d.(.@.................................0..%.e..........0......... +............ +19:12:26.641168 IP6 ::1.8080 > ::1.32816: Flags [R.], seq 0, ack 630744337, win 0, length 0 +`.$....@...................................0....%.e.P....... +19:12:43.027807 IP 127.0.0.1.41602 > 127.0.0.1.8080: Flags [S], seq 851489407, win 65495, options [mss 65495,sackOK,TS val 2462005530 ecr 0,nop,wscale 7], length 0 +E..<.y@.@..@............2............0......... +..9......... +19:12:43.027811 IP 127.0.0.1.8080 > 127.0.0.1.41602: Flags [R.], seq 0, ack 851489408, win 0, length 0 +E..(..@.@.<.................2...P... +{.. +19:12:43.027836 IP6 ::1.34804 > ::1.8080: Flags [S], seq 3910870731, win 65476, options [mss 65476,sackOK,TS val 2676421784 ecr 0,nop,wscale 7], length 0 +`..Q.(.@......................................&..........0......... +............ +19:12:43.027839 IP6 ::1.8080 > ::1.34804: Flags [R.], seq 0, ack 3910870732, win 0, length 0 +`......@..........................................&.P....... +19:12:54.413810 IP 127.0.0.1.52472 > 127.0.0.1.8080: Flags [S], seq 1293846705, win 65495, options [mss 65495,sackOK,TS val 2462016916 ecr 0,nop,wscale 7], length 0 +E..<.H@.@.[q............M............0......... +..e......... +19:12:54.413816 IP 127.0.0.1.8080 > 127.0.0.1.52472: Flags [R.], seq 0, ack 1293846706, win 0, length 0 +E..(..@.@.<.................M...P....t.. +19:12:54.413855 IP6 ::1.50118 > ::1.8080: Flags [S], seq 971549680, win 65476, options [mss 65476,sackOK,TS val 2676433170 ecr 0,nop,wscale 7], length 0 +`.|s.(.@....................................9............0......... +..!......... +19:12:54.413860 IP6 ::1.8080 > ::1.50118: Flags [R.], seq 0, ack 971549681, win 0, length 0 +`.+f...@........................................9...P....... +19:13:30.465338 IP 127.0.0.1.45936 > 127.0.0.1.8080: Flags [S], seq 499019428, win 65495, options [mss 65495,sackOK,TS val 2462052968 ecr 0,nop,wscale 7], length 0 +E.. 127.0.0.1.45936: Flags [R.], seq 0, ack 499019429, win 0, length 0 +E..(..@.@.<............p......n.P...Rj.. +19:13:30.465371 IP6 ::1.45326 > ::1.8080: Flags [S], seq 2488604191, win 65476, options [mss 65476,sackOK,TS val 2676469222 ecr 0,nop,wscale 7], length 0 +`.J .(.@.....................................U...........0......... +............ +19:13:30.465375 IP6 ::1.8080 > ::1.45326: Flags [R.], seq 0, ack 2488604192, win 0, length 0 +`../...@.........................................U. P....... +21:33:29.974894 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [S], seq 289262757, win 65495, options [mss 65495,sackOK,TS val 2470452477 ecr 0,nop,wscale 7], length 0 +E..<.$@.@............~...=...........0......... +.@.......... +21:33:29.974902 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [S.], seq 2132570133, ack 289262758, win 65483, options [mss 65495,sackOK,TS val 2470452477 ecr 2470452477,nop,wscale 7], length 0 +E..<..@.@.<............~..p..=.......0......... +.@...@...... +21:33:29.974907 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2470452477 ecr 2470452477], length 0 +E..4.%@.@............~...=....p......(..... +.@...@.. +21:33:29.974939 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 512, options [nop,nop,TS val 2470452477 ecr 2470452477], length 175: HTTP: POST /notes HTTP/1.1 +E....&@.@............~...=....p............ +.@...@..POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/7.81.0 +Accept: */* +Content-Type: application/json +Content-Length: 39 + +{"title":"trace me","body":"in flight"} +21:33:29.974940 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [.], ack 176, win 511, options [nop,nop,TS val 2470452477 ecr 2470452477], length 0 +E..4.+@.@.$............~..p..=.U.....(..... +.@...@.. +21:33:29.980128 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [P.], seq 1:207, ack 176, win 512, options [nop,nop,TS val 2470452482 ecr 2470452477], length 206: HTTP: HTTP/1.1 201 Created +E....,@.@.#............~..p..=.U........... +.@...@..HTTP/1.1 201 Created +Content-Type: application/json +Date: Wed, 17 Jun 2026 18:33:29 GMT +Content-Length: 93 + +{"id":5,"title":"trace me","body":"in flight","created_at":"2026-06-17T18:33:29.975704318Z"} + +21:33:29.980139 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 207, win 511, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.'@.@............~...=.U..p......(..... +.@...@.. +21:33:29.980234 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [F.], seq 176, ack 207, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.(@.@............~...=.U..p......(..... +.@...@.. +21:33:29.980286 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [F.], seq 207, ack 177, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.-@.@.$............~..p..=.V.....(..... +.@...@.. +21:33:29.980293 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 208, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.)@.@............~...=.V..p......(..... +.@...@.. +21:34:41.834814 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [S], seq 1421564529, win 65495, options [mss 65495,sackOK,TS val 2470524337 ecr 0,nop,wscale 7], length 0 +E.. 127.0.0.1.47718: Flags [S.], seq 2578938132, ack 1421564530, win 65483, options [mss 65495,sackOK,TS val 2470524337 ecr 2470524337,nop,wscale 7], length 0 +E..<..@.@.<............f..y.T.Zr.....0......... +.A5..A5..... +21:34:41.834828 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2470524337 ecr 2470524337], length 0 +E..4g,@.@............f..T.Zr..y......(..... +.A5..A5. +21:34:41.834861 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 512, options [nop,nop,TS val 2470524337 ecr 2470524337], length 175: HTTP: POST /notes HTTP/1.1 +E...g-@.@............f..T.Zr..y............ +.A5..A5.POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/7.81.0 +Accept: */* +Content-Type: application/json +Content-Length: 39 + +{"title":"trace me","body":"in flight"} +21:34:41.834863 IP 127.0.0.1.8080 > 127.0.0.1.47718: Flags [.], ack 176, win 511, options [nop,nop,TS val 2470524337 ecr 2470524337], length 0 +E..4.i@.@..X...........f..y.T.[!.....(..... +.A5..A5. +21:34:41.839333 IP 127.0.0.1.8080 > 127.0.0.1.47718: Flags [P.], seq 1:207, ack 176, win 512, options [nop,nop,TS val 2470524341 ecr 2470524337], length 206: HTTP: HTTP/1.1 201 Created +E....j@.@..............f..y.T.[!........... +.A5..A5.HTTP/1.1 201 Created +Content-Type: application/json +Date: Wed, 17 Jun 2026 18:34:41 GMT +Content-Length: 93 + +{"id":6,"title":"trace me","body":"in flight","created_at":"2026-06-17T18:34:41.835071034Z"} + +21:34:41.839349 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [.], ack 207, win 511, options [nop,nop,TS val 2470524342 ecr 2470524341], length 0 +E..4g.@.@............f..T.[!..y......(..... +.A5..A5. +21:34:41.839506 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [F.], seq 176, ack 207, win 512, options [nop,nop,TS val 2470524342 ecr 2470524341], length 0 +E..4g/@.@............f..T.[!..y......(..... +.A5..A5. diff --git a/monitoring/grafana/provisioning/dashboards/dashboard.yml b/monitoring/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 000000000..5bc28e0bf --- /dev/null +++ b/monitoring/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: 'QuickNotes' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/monitoring/grafana/provisioning/dashboards/golden-signals.json b/monitoring/grafana/provisioning/dashboards/golden-signals.json new file mode 100644 index 000000000..8eef2e564 --- /dev/null +++ b/monitoring/grafana/provisioning/dashboards/golden-signals.json @@ -0,0 +1,56 @@ +{ + "title": "QuickNotes Golden Signals", + "uid": "quicknotes-golden", + "panels": [ + { + "id": 1, + "title": "Latency (p95)", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(quicknotes_request_duration_seconds_bucket[5m]))", + "legendFormat": "p95 latency" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0} + }, + { + "id": 2, + "title": "Traffic (requests/sec)", + "type": "graph", + "targets": [ + { + "expr": "rate(quicknotes_http_requests_total[5m])", + "legendFormat": "req/s" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0} + }, + { + "id": 3, + "title": "Error Ratio", + "type": "graph", + "targets": [ + { + "expr": "sum(rate(quicknotes_http_requests_total{status=~\"4..|5..\"}[5m])) / sum(rate(quicknotes_http_requests_total[5m]))", + "legendFormat": "error ratio" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} + }, + { + "id": 4, + "title": "Saturation (notes count)", + "type": "graph", + "targets": [ + { + "expr": "quicknotes_notes_total", + "legendFormat": "total notes" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8} + } + ], + "schemaVersion": 27, + "version": 1 +} \ No newline at end of file diff --git a/monitoring/grafana/provisioning/datasources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 000000000..8049912b1 --- /dev/null +++ b/monitoring/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 000000000..563c1f3a9 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'quicknotes' + static_configs: + - targets: ['quicknotes:8080'] \ No newline at end of file diff --git a/report.html b/report.html new file mode 100644 index 000000000..fac19b3c0 --- /dev/null +++ b/report.html @@ -0,0 +1,420 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

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

+ +

+ Generated on Wed, 8 Jul 2026 17:48:46 +

+ +

+ ZAP Version: 2.16.0 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
1
+
+
Informational
+
+
0
+
+
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
+
+ + + +

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
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/report.json b/report.json new file mode 100644 index 000000000..85b24a7ce --- /dev/null +++ b/report.json @@ -0,0 +1,49 @@ +{ + "@programName": "ZAP", + "@version": "2.16.0", + "@generated": "Wed, 8 Jul 2026 17:48:46", + "created": "2026-07-08T17:48:46.648068637Z", + "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": "0", + "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": "1" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/submissions/lab10.md b/submissions/lab10.md new file mode 100644 index 000000000..2d971ae53 --- /dev/null +++ b/submissions/lab10.md @@ -0,0 +1,100 @@ +

Task 1

+ +add yml in .github/workflows/release.yml + +```git tag -a -s v0.1.0 -m "Lab 10 release"``` + +```git push origin v0.1.0``` + +Enumerating objects: 1, done. +Counting objects: 100% (1/1), done. +Writing objects: 100% (1/1), 381 bytes | 381.00 KiB/s, done. +Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) +To https://github.com/sovva6-14/DevOps-Intro.git + * [new tag] v0.1.0 -> v0.1.0 + +

Questions:

+ +a) OIDC vs GITHUB_TOKEN - for pushing to ghcr.io from the same repo, GITHUB_TOKEN with packages: write is enough. When would you reach for OIDC instead, and what does it give you that GITHUB_TOKEN doesn't? + +You would use OIDC (OpenID Connect) instead of GITHUB_TOKEN when: + +* Pushing to external registries: OIDC allows GitHub Actions to authenticate to AWS ECR, Google Artifact Registry, or Azure Container Registry without storing long-lived credentials as secrets +* Cross-repository access: If you need to push images from repo A to a container registry owned by repo B, OIDC with workload identity federation provides fine-grained, short-lived access +* Audit trail: OIDC provides a verifiable token that links the authentication request to the specific workflow run and GitHub repository, improving auditability + +OIDC benefits over GITHUB_TOKEN: + +* No secret rotation: OIDC issues short-lived tokens (up to 5 minutes) that are automatically rotated, eliminating secret management overhead +* Federated identity: Works across cloud providers without sharing credentials +* Fine-grained permissions: You can scope OIDC to specific workload identities, not just the entire repository +* Security: Reduces the risk of leaked credentials since tokens are ephemeral and cannot be used outside the workflow context + +b) :latest tag vs :v0.1.0 immutable tag - Lab 6 covered why :latest is mutable. So why do you still ship a :latest tag alongside the immutable one in production releases? + +You ship :latest alongside the immutable version tag because: + +* Development convenience: :latest provides a stable, well-known reference for developers and staging environments who want to always use the newest version without updating manifests. +* Rollback simplicity: In case of deployment issues, you can quickly roll back by pointing :latest to a previous commit without changing CI configuration. +* Demo/quick-start purposes: New users or evaluators can simply docker pull :latest to get the most recent release without knowing the exact version number. +* Documentation simplicity: Tutorials and examples can reference :latest instead of updating documentation for every release. + +The immutable tag (:v0.1.0) ensures: + +* Exact traceability for production deployments +* Reproducible builds +* Ability to run multiple versions simultaneously + +c) packages: write scope only - what's the principle, and what concrete attack does the narrow scope prevent vs write: all? + +The principle is least privilege - granting only the minimum permissions necessary to perform the task + +Concrete attack prevented: If an attacker compromises the CI workflow (e.g., through a malicious action or dependency), a packages: write scope limits the damage to: + +* Pushing/pulling container images to ghcr.io +* Modifying package metadata + +With write: all, the attacker would gain: + +* Write access to repository code (push commits, delete branches) +* Access to all repository secrets (including potential AWS/Azure keys) +* Ability to create malicious releases or tags +* Write access to GitHub Pages, issues, project boards, and all other repository resources + +The narrow scope prevents privilege escalation: even if an attacker can push a malicious image, they cannot modify the repository source code, steal deployment credentials, or compromise other integrated services + +

Task 2

+ +I ran into a problem after registering on the site. I received a confirmation email, but when I clicked the link to activate my account, I got an "Access denied" error (even though I was using a VPN). In https://cdn-uploads.huggingface.co access denied (lol) + +AccessDeniedAccess Denied +This XML file does not appear to have any style information associated with it. The document tree is shown below. + +AccessDenied +Access Denied +... + + +

Questions:

+ +d) HF Spaces "sleep" vs Cloud Run "scale to zero" — same idea, different orders of magnitude. Why is HF's wake so much slower? What does the platform optimize for differently? + +HF Spaces is optimized for developer convenience and cost, not performance: +* Underlying infrastructure: HF Spaces runs on shared, lower-performance compute (likely older CPUs with limited network bandwidth) compared to Cloud Run's SSD-backed, high-speed infrastructure +* Container cold start: HF pulls the image from scratch on each cold start (no image pre-warming), while Cloud Run maintains a warm pool of pre-loaded container images +* Resource contention: HF Spaces are multi-tenant with less aggressive resource isolation, leading to slower startup when resources are contended +* Different trade-off: HF prioritizes a free, accessible platform for ML demos over fast cold starts. Cloud Run optimizes for enterprise workloads where sub-second scaling is business-critical + +e) Why does the Space need app_port: 8080? What's HF's default and why do they default to that? + +* HF Default: 7860 (used by Gradio and Streamlit apps) +* Why 7860: This is the default port for Gradio interfaces, which are the most common application type on HF Spaces. Many ML demos use Gradio, so HF optimized for this default +* Why we need to change it: QuickNotes is a Go REST API that listens on port 8080 (a common backend API port). If we don't set app_port: 8080, HF would try to connect to port 7860, find nothing listening, and mark the Space as "failed" + +f) You pulled the image from ghcr.io into the Space. What's the trade-off vs building the Dockerfile inside the Space? (Hint: caching, reproducibility, debug-ability) + +See the table in section 2.1 for details. The key trade-offs are: + +* Caching: Pulling uses HF's layer cache (fast), while building inside the Space must download dependencies on every deploy (slow). +* Reproducibility: Pulling ensures the exact same binary runs everywhere, while building inside the Space can produce different results due to timestamp changes or network variations. +* Debug-ability: Pulling allows local testing of the same image before pushing, while building inside the Space is a "black box" that's hard to debug when it fails. diff --git a/submissions/lab2.md b/submissions/lab2.md new file mode 100644 index 000000000..7b95b50d1 --- /dev/null +++ b/submissions/lab2.md @@ -0,0 +1,160 @@ +#important work + + +```git rev-parse HEAD``` + +66bbd4db9228bc9a4cab7439746b993749c026ab + +```git cat-file -t HEAD``` + +commit + +```git cat-file -p HEAD``` + +tree 20bda2b2625085720751a3e794f82e5625a409b3 +parent 170000c9d1b5e90a37b6f1a9b826552d53051773 +author Dmitrii Creed 1780392934 +0400 +committer Dmitrii Creed 1780394046 +0400 + +```git cat-file -p 20bda2b2625085720751a3e794f82e5625a409b3``` + +100644 blob 1c0a1e94b7bbdd951f456cda51af6b8484cc3cee .gitignore +100644 blob d10c04c6e7e0014f4fe883599c11747c15012d4e README.md +040000 tree 7d0898a908e274ea809722844cdbd836f3b1c05a app +040000 tree 6db686e340ecdd318fa43375e26254293371942a labs +040000 tree 3f11973a71be5915539cb53313149aa319d69cb5 lectures + +```git cat-file -p 7d0898a908e274ea809722844cdbd836f3b1c05a``` + +100644 blob 8ba1a5234925005265281bf7809153487097373c .golangci.yml +100644 blob 24ab0258318f4aac6ec7d3a924a1d6f05209b446 Makefile +100644 blob 1aed7f8904100182d3fb4e1b90dbf3bd5a126beb README.md +100644 blob b76e91cf916dcebc1d6898e22012c737c117003a go.mod +100644 blob c534979c5a3aa0e032fb61e1562d4bd343ecaf4c handlers.go +100644 blob 9dff2e3e5b734f9afa4bc26c30d784bee8aa327c handlers_test.go +100644 blob e258ffcfe44ebc6923eb78d51c63fc2317aa1dfd main.go +100644 blob ecf4fd2edd38dcbc82459122660aa424342f9148 seed.json +100644 blob 4a9ca2b3a371cc43f8762095a6944cd96ea7d7d0 store.go +100644 blob 3b8ff9d45ae9e6781ffe333ccc7eff40da35a1bf store_test.go + +```ls -la .git/``` + +total 25 +drwxr-xr-x 1 UserName 197121 0 Jun 9 15:17 ./ +drwxr-xr-x 1 UserName 197121 0 Jun 9 15:18 ../ +-rw-r--r-- 1 UserName 197121 80 Jun 8 00:20 COMMIT_EDITMSG +-rw-r--r-- 1 UserName 197121 189 Jun 8 00:34 config +-rw-r--r-- 1 UserName 197121 73 Jun 4 00:25 description +-rw-r--r-- 1 UserName 197121 0 Jun 9 15:22 FETCH_HEAD +-rw-r--r-- 1 UserName 197121 29 Jun 9 15:17 HEAD +drwxr-xr-x 1 UserName 197121 0 Jun 4 00:25 hooks/ +-rw-r--r-- 1 UserName 197121 3055 Jun 9 15:16 index +drwxr-xr-x 1 UserName 197121 0 Jun 4 00:25 info/ +drwxr-xr-x 1 UserName 197121 0 Jun 4 00:25 logs/ +drwxr-xr-x 1 UserName 197121 0 Jun 9 15:22 objects/ +-rw-r--r-- 1 UserName 197121 46 Jun 4 00:32 packed-refs +drwxr-xr-x 1 UserName 197121 0 Jun 4 00:25 refs/ + +```cat .git/HEAD``` + +ref: refs/heads/feature/lab2 + +```ls .git/refs/heads/``` + +feature/ main + +```ls .git/objects/ | head``` + +0a/ +0c/ +0e/ +0f/ +13/ +1a/ +27/ +38/ +3a/ +40/ + +```find .git/objects -type f | wc -l``` + +29 + + +```git log --oneline``` + +66bbd4d (HEAD -> feature/lab2, main) docs(lab1): align Task 3 GitHub Community engagement with other courses +170000c Merge pull request #907 from inno-devops-labs/s26-refactor +d50436c fix(lab12,gitignore): Spin SDK (WAGI removed in Spin 3.x); minimal student-safe gitignore +4705a3d fix(.gitignore): stop ignoring submissions/ +4082340 docs(grading,lab11,lab12): bonus labs to 4+4+2; grading rebalanced to 70-14-5-20-30 = 139% +7b16dc5 docs(lab10): switch deploy targets to card-free platforms — HF Spaces + Cloudflare Tunnel +4a05efa docs(labs): scaffold the skill — labs 5-12 stop handing students copy-paste answers +8387fb9 docs(lab3): scaffold the skill — students write their own CI yaml; GitLab as parallel path +983fba0 docs(course): rewrite README + add .gitignore for project-threaded structure +7914e37 docs(labs): refactor 12 labs to 6+4+2 (lab1) / 6+4+bonus (lab2-10) / 10pts (lab11-12) +aa5aa1c docs(lectures): rewrite lec1-10 + add reading11/12 for project-threaded course +b8fc480 feat(app): introduce QuickNotes Go service for project-threaded course +6f044dd Replace IPFS with Nix +0a87e1c refactor: reduce prescriptiveness in GitLab CI instructions +eaea715 feat: add GitLab CI alternative instructions to lab3 +d6b6a03 Update lab2 +87810a0 feat: remove old Exam Exemption Policy +1e1c32b feat: update structure +6c27ee7 feat: publish lecs 9 & 10 +1826c36 feat: update lab7 +3049f08 feat: publish lec8 +da8f635 feat: introduce all labs and revised structure +04b174e feat: publish lab and lec #5 +67f12f1 feat: publish labs 4&5, revise others +82d1989 feat: publish lab3 and lec3 +3f80c83 feat: publish lec2 +499f2ba feat: publish lab2 +af0da89 feat: update lab1 +74a8c27 Publish lab1 +f0485c0 Publish lec1 +31dd11b Publish README.md + +```git reflog``` + +66bbd4d (HEAD -> feature/lab2, main) HEAD@{0}: reset: moving to HEAD~2 +03cdb25 HEAD@{1}: commit: lab1 +9a2873e HEAD@{2}: commit: wip(lab2): Work progress +66bbd4d (HEAD -> feature/lab2, main) HEAD@{3}: checkout: moving from main to feature/lab2 +66bbd4d (HEAD -> feature/lab2, main) HEAD@{4}: checkout: moving from feature/lab1 to main +dce058f (feature/lab1) HEAD@{5}: checkout: moving from main to feature/lab1 +66bbd4d (HEAD -> feature/lab2, main) HEAD@{6}: checkout: moving from feature/lab1 to main +dce058f (feature/lab1) HEAD@{7}: commit: docs(lab1): start submission +66bbd4d (HEAD -> feature/lab2, main) HEAD@{8}: checkout: moving from main to feature/lab1 +66bbd4d (HEAD -> feature/lab2, main) HEAD@{9}: clone: from https://github.com/sovva6-14/DevOps-Intro.git + +```git reset --hard 9a2873e``` + +HEAD is now at 9a2873e wip(lab2): Work progress + +```git push origin "v0.1.0-lab2-Sovva"``` + + +Enumerating objects: 1, done. +Counting objects: 100% (1/1), done. +Writing objects: 100% (1/1), 420 bytes | 420.00 KiB/s, done. +Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) +To https://github.com/sovva6-14/DevOps-Intro.git + * [new tag] v0.1.0-lab2-Sovva -> v0.1.0-lab2-Sovva + + ```git commit -S -s --allow-empty -m "docs: upstream moved while you worked"``` + +[feature/lab2 04b5e86] docs: upstream moved while you worked + + +```git push --force-with-lease origin feature/lab2``` + +Enumerating objects: 14, done. +Counting objects: 100% (14/14), done. +Delta compression using up to 20 threads +Compressing objects: 100% (10/10), done. +Writing objects: 100% (13/13), 3.95 KiB | 3.96 MiB/s, done. +Total 13 (delta 7), reused 6 (delta 1), pack-reused 0 (from 0) +remote: Resolving deltas: 100% (7/7), completed with 1 local object. +To https://github.com/sovva6-14/DevOps-Intro.git + + be131f7...04b5e86 feature/lab2 -> feature/lab2 (forced update) \ No newline at end of file diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..cbe6040f1 --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,3 @@ +#GitHub Actions + +Action: https://github.com/sovva6-14/DevOps-Intro/actions/runs/27701703406 \ No newline at end of file diff --git a/submissions/lab4.md b/submissions/lab4.md new file mode 100644 index 000000000..d69f00247 --- /dev/null +++ b/submissions/lab4.md @@ -0,0 +1,230 @@ +

Task 1

+ +```sudo tcpdump -r '/mnt/c/Users/Sovva/Desktop/DevOps-Intro/lab4-trace.pcap' -nn -A | tee lab4-trace.txt``` + +reading from file /mnt/c/Users/Sovva/Desktop/DevOps-Intro/lab4-trace.pcap, link-type EN10MB (Ethernet), snapshot length 262144 +19:11:19.932815 IP 127.0.0.1.42566 > 127.0.0.1.8080: Flags [S], seq 3179416048, win 65495, options [mss 65495,sackOK,TS val 2461922435 ecr 0,nop,wscale 7], length 0 +E..<..@.@.(..........F.... ..........0......... +............ +19:11:19.932821 IP 127.0.0.1.8080 > 127.0.0.1.42566: Flags [R.], seq 0, ack 3179416049, win 0, length 0 +E..(..@.@.<............F...... .P...$... +19:11:19.932861 IP6 ::1.40002 > ::1.8080: Flags [S], seq 2374255427, win 65476, options [mss 65476,sackOK,TS val 2676338689 ecr 0,nop,wscale 7], length 0 +`.~..(.@.................................B....CC.........0......... +............ +19:11:19.932866 IP6 ::1.8080 > ::1.40002: Flags [R.], seq 0, ack 2374255428, win 0, length 0 +`.k9...@...................................B......CDP....... +19:12:26.641138 IP 127.0.0.1.41710 > 127.0.0.1.8080: Flags [S], seq 509650619, win 65495, options [mss 65495,sackOK,TS val 2461989143 ecr 0,nop,wscale 7], length 0 +E..<1.@.@. +..............`...........0......... +............ +19:12:26.641142 IP 127.0.0.1.8080 > 127.0.0.1.41710: Flags [R.], seq 0, ack 509650620, win 0, length 0 +E..(..@.@.<..................`..P...*3.. +19:12:26.641165 IP6 ::1.32816 > ::1.8080: Flags [S], seq 630744336, win 65476, options [mss 65476,sackOK,TS val 2676405397 ecr 0,nop,wscale 7], length 0 +` .d.(.@.................................0..%.e..........0......... +............ +19:12:26.641168 IP6 ::1.8080 > ::1.32816: Flags [R.], seq 0, ack 630744337, win 0, length 0 +`.$....@...................................0....%.e.P....... +19:12:43.027807 IP 127.0.0.1.41602 > 127.0.0.1.8080: Flags [S], seq 851489407, win 65495, options [mss 65495,sackOK,TS val 2462005530 ecr 0,nop,wscale 7], length 0 +E..<.y@.@..@............2............0......... +..9......... +19:12:43.027811 IP 127.0.0.1.8080 > 127.0.0.1.41602: Flags [R.], seq 0, ack 851489408, win 0, length 0 +E..(..@.@.<.................2...P... +{.. +19:12:43.027836 IP6 ::1.34804 > ::1.8080: Flags [S], seq 3910870731, win 65476, options [mss 65476,sackOK,TS val 2676421784 ecr 0,nop,wscale 7], length 0 +`..Q.(.@......................................&..........0......... +............ +19:12:43.027839 IP6 ::1.8080 > ::1.34804: Flags [R.], seq 0, ack 3910870732, win 0, length 0 +`......@..........................................&.P....... +19:12:54.413810 IP 127.0.0.1.52472 > 127.0.0.1.8080: Flags [S], seq 1293846705, win 65495, options [mss 65495,sackOK,TS val 2462016916 ecr 0,nop,wscale 7], length 0 +E..<.H@.@.[q............M............0......... +..e......... +19:12:54.413816 IP 127.0.0.1.8080 > 127.0.0.1.52472: Flags [R.], seq 0, ack 1293846706, win 0, length 0 +E..(..@.@.<.................M...P....t.. +19:12:54.413855 IP6 ::1.50118 > ::1.8080: Flags [S], seq 971549680, win 65476, options [mss 65476,sackOK,TS val 2676433170 ecr 0,nop,wscale 7], length 0 +`.|s.(.@....................................9............0......... +..!......... +19:12:54.413860 IP6 ::1.8080 > ::1.50118: Flags [R.], seq 0, ack 971549681, win 0, length 0 +`.+f...@........................................9...P....... +19:13:30.465338 IP 127.0.0.1.45936 > 127.0.0.1.8080: Flags [S], seq 499019428, win 65495, options [mss 65495,sackOK,TS val 2462052968 ecr 0,nop,wscale 7], length 0 +E.. 127.0.0.1.45936: Flags [R.], seq 0, ack 499019429, win 0, length 0 +E..(..@.@.<............p......n.P...Rj.. +19:13:30.465371 IP6 ::1.45326 > ::1.8080: Flags [S], seq 2488604191, win 65476, options [mss 65476,sackOK,TS val 2676469222 ecr 0,nop,wscale 7], length 0 +`.J .(.@.....................................U...........0......... +............ +19:13:30.465375 IP6 ::1.8080 > ::1.45326: Flags [R.], seq 0, ack 2488604192, win 0, length 0 +`../...@.........................................U. P....... +21:33:29.974894 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [S], seq 289262757, win 65495, options [mss 65495,sackOK,TS val 2470452477 ecr 0,nop,wscale 7], length 0 +E..<.$@.@............~...=...........0......... +.@.......... +21:33:29.974902 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [S.], seq 2132570133, ack 289262758, win 65483, options [mss 65495,sackOK,TS val 2470452477 ecr 2470452477,nop,wscale 7], length 0 +E..<..@.@.<............~..p..=.......0......... +.@...@...... +21:33:29.974907 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2470452477 ecr 2470452477], length 0 +E..4.%@.@............~...=....p......(..... +.@...@.. +21:33:29.974939 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 512, options [nop,nop,TS val 2470452477 ecr 2470452477], length 175: HTTP: POST /notes HTTP/1.1 +E....&@.@............~...=....p............ +.@...@..POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/7.81.0 +Accept: */* +Content-Type: application/json +Content-Length: 39 + +{"title":"trace me","body":"in flight"} +21:33:29.974940 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [.], ack 176, win 511, options [nop,nop,TS val 2470452477 ecr 2470452477], length 0 +E..4.+@.@.$............~..p..=.U.....(..... +.@...@.. +21:33:29.980128 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [P.], seq 1:207, ack 176, win 512, options [nop,nop,TS val 2470452482 ecr 2470452477], length 206: HTTP: HTTP/1.1 201 Created +E....,@.@.#............~..p..=.U........... +.@...@..HTTP/1.1 201 Created +Content-Type: application/json +Date: Wed, 17 Jun 2026 18:33:29 GMT +Content-Length: 93 + +{"id":5,"title":"trace me","body":"in flight","created_at":"2026-06-17T18:33:29.975704318Z"} + +21:33:29.980139 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 207, win 511, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.'@.@............~...=.U..p......(..... +.@...@.. +21:33:29.980234 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [F.], seq 176, ack 207, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.(@.@............~...=.U..p......(..... +.@...@.. +21:33:29.980286 IP 127.0.0.1.8080 > 127.0.0.1.54142: Flags [F.], seq 207, ack 177, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.-@.@.$............~..p..=.V.....(..... +.@...@.. +21:33:29.980293 IP 127.0.0.1.54142 > 127.0.0.1.8080: Flags [.], ack 208, win 512, options [nop,nop,TS val 2470452482 ecr 2470452482], length 0 +E..4.)@.@............~...=.V..p......(..... +.@...@.. +21:34:41.834814 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [S], seq 1421564529, win 65495, options [mss 65495,sackOK,TS val 2470524337 ecr 0,nop,wscale 7], length 0 +E.. 127.0.0.1.47718: Flags [S.], seq 2578938132, ack 1421564530, win 65483, options [mss 65495,sackOK,TS val 2470524337 ecr 2470524337,nop,wscale 7], length 0 +E..<..@.@.<............f..y.T.Zr.....0......... +.A5..A5..... +21:34:41.834828 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2470524337 ecr 2470524337], length 0 +E..4g,@.@............f..T.Zr..y......(..... +.A5..A5. +21:34:41.834861 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 512, options [nop,nop,TS val 2470524337 ecr 2470524337], length 175: HTTP: POST /notes HTTP/1.1 +E...g-@.@............f..T.Zr..y............ +.A5..A5.POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/7.81.0 +Accept: */* +Content-Type: application/json +Content-Length: 39 + +{"title":"trace me","body":"in flight"} +21:34:41.834863 IP 127.0.0.1.8080 > 127.0.0.1.47718: Flags [.], ack 176, win 511, options [nop,nop,TS val 2470524337 ecr 2470524337], length 0 +E..4.i@.@..X...........f..y.T.[!.....(..... +.A5..A5. +21:34:41.839333 IP 127.0.0.1.8080 > 127.0.0.1.47718: Flags [P.], seq 1:207, ack 176, win 512, options [nop,nop,TS val 2470524341 ecr 2470524337], length 206: HTTP: HTTP/1.1 201 Created +E....j@.@..............f..y.T.[!........... +.A5..A5.HTTP/1.1 201 Created +Content-Type: application/json +Date: Wed, 17 Jun 2026 18:34:41 GMT +Content-Length: 93 + +{"id":6,"title":"trace me","body":"in flight","created_at":"2026-06-17T18:34:41.835071034Z"} + +21:34:41.839349 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [.], ack 207, win 511, options [nop,nop,TS val 2470524342 ecr 2470524341], length 0 +E..4g.@.@............f..T.[!..y....tcpdump: pcap_loop: truncated dump file; tried to read 16 header bytes, only got 2 +..(..... +.A5..A5. +21:34:41.839506 IP 127.0.0.1.47718 > 127.0.0.1.8080: Flags [F.], seq 176, ack 207, win 512, options [nop,nop,TS val 2470524342 ecr 2470524341], length 0 +E..4g/@.@............f..T.[!..y......(..... +.A5..A5. + +```ss -tlnp | grep :8080``` + +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=17972,fd=3)) + +```ip route show``` + +default via 172.29.0.1 dev eth0 proto kernel +172.29.0.0/20 dev eth0 proto kernel scope link src 172.29.1.206 + +```mtr -rwc 5 localhost``` + +Start: 2026-06-17T21:40:31+0300 +HOST: DESKTOP-5IK3AVO Loss% Snt Last Avg Best Wrst StDev + 1.|-- localhost 0.0% 5 0.0 0.0 0.0 0.0 0.0 + +```dig +short example.com @1.1.1.1``` + +172.66.147.243 +104.20.23.154 + +```journalctl --user -u quicknotes -n 20 || true``` +-- No entries -- + + +```what would you check first if QuickNotes returned 502?``` + +Check logs, metrics (if we have). Commands: + + * ss --tulnp - for check worked process, ports, service, network sockets + * htop - for monitoring CPU, RAM, process, hardware, uptime, tasks, etc. + + +

Task 2

+ +```ADDR=:8080 go run . & PID1=$!``` + +60505 + + +```ADDR=:8080 go run . 2>&1 | tee /tmp/qn-broken.log & PID2=$!``` + +60645 +2026/06/17 22:16:40 quicknotes listening on :8080 (notes loaded: 6) +2026/06/17 22:16:40 listen: listen tcp :8080: bind: address already in use + +```ps -ef | grep "go run" | grep -v grep``` +sovva 60505 350 0 22:16 pts/0 00:00:00 go run . + +```ps -ef | grep quicknotes``` +sovva 60555 60505 0 22:16 pts/0 00:00:00 /home/sovva/.cache/go-build/2c/2cd7be73141e97a246ae4b4915a627a9b587f70c01aaaa6a881dfee0c09b54d7-d/quicknotes +sovva 61508 350 0 22:19 pts/0 00:00:00 grep --color=auto quicknotes + +```ss -tlnp | grep 8080``` + +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=60555,fd=3)) + +```curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/health``` + +200 + +```sudo iptables -L -n -v 2>/dev/null || sudo nft list ruleset 2>/dev/null || true``` + +Chain INPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + +Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + +Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + +```dig +short localhost``` + +127.0.0.1 + + +```sleep 1``` + +Terminated ADDR=:8080 go run . + +```ADDR=:8080 go run . & sleep 1``` + +62116 +2026/06/17 22:21:18 quicknotes listening on :8080 (notes loaded: 6) +2026/06/17 22:21:18 listen: listen tcp :8080: bind: address already in use +exit status 1 +Exit 1 ADDR=:8080 go run . + +```curl -s http://localhost:8080/health``` + +{"notes":6,"status":"ok"} \ No newline at end of file diff --git a/submissions/lab5.md b/submissions/lab5.md new file mode 100644 index 000000000..7e3ef179b --- /dev/null +++ b/submissions/lab5.md @@ -0,0 +1,87 @@ +

Task 1

+ +```vagrant --version``` + +Vagrant 2.4.9 + +```vagrant up --provider virtualbox``` + +Bringing machine 'default' up with 'virtualbox' provider... +==> default: Box 'ubuntu/jammy64' could not be found. Attempting to find and install... + default: Box Provider: virtualbox + default: Box Version: >= 0 +==> default: Loading metadata for box 'ubuntu/jammy64' + default: URL: https://vagrantcloud.com/api/v2/vagrant/ubuntu/jammy64 +==> default: Adding box 'ubuntu/jammy64' (v20241002.0.0) for provider: virtualbox + default: Downloading: https://vagrantcloud.com/ubuntu/boxes/jammy64/versions/20241002.0.0/providers/virtualbox/unknown/vagrant.box +==> default: Successfully added box 'ubuntu/jammy64' (v20241002.0.0) for 'virtualbox'! +==> default: Importing base box 'ubuntu/jammy64'... +==> default: Matching MAC address for NAT networking... +==> default: Checking if box 'ubuntu/jammy64' version '20241002.0.0' is up to date... +==> default: Setting the name of the VM: DevOps-Intro_default_1782309021968_643 +==> default: Clearing any previously set network interfaces... +==> default: Preparing network interfaces based on configuration... + default: Adapter 1: nat +==> default: Forwarding ports... + default: 8080 (guest) => 18080 (host) (adapter 1) + default: 22 (guest) => 2222 (host) (adapter 1) +==> default: Running 'pre-boot' VM customizations... +==> default: Booting VM.. + +

Questions:

+ +```a) Synced folders: Vagrant supports nfs, rsync, virtualbox, and smb mount types. Which did you pick and why? What's the trade-off?``` + +rsync - easy to setup ;) + +```b) NAT vs Bridged vs Host-only: which network mode are you using (it's the default, but say which it is)? Why is 127.0.0.1-bound port forwarding safer than a Bridged interface for a course exercise?``` + + NAT it's default mode for VirtuaBox. 127.0.0.1 == localhost. This is safer for it's study exercise + +```c) Provisioning options: Vagrant supports shell, ansible, ansible_local, puppet, chef, … which did you pick for installing Go and why?``` + +shell-provisioning does't require installation additional tools (Ansible, etc) + +```d) Why pin Go to a specific point release (1.24.5) instead of 1.24?``` + +1.2.4 it's major version + + +

Task 2

+ +```vagrant snapshot save working-state``` + +==> default: Snapshotting the machine as 'working-state'... +==> default: Snapshot saved! You can restore the snapshot at any time by +==> default: using `vagrant snapshot restore`. You can delete it using +==> default: `vagrant snapshot delete`. + + +```vagrant ssh -c 'go version'`` + +/usr/local/go/bin/go: No such file or directory + + +```time vagrant snapshot restore working-state``` + +real 0m3.404s +user 0m0.519s +sys 0m0.020s + +```vagrant ssh -c 'go version'``` + +go version go1.24.5 linux/amd64 + +

Questions:

+ +```e) Snapshots are not backups. Explain why in 2-3 sentences — what failure modes is a snapshot useless for?``` + +Snapshot - for quick rollback. Backup - for long-term pretection and recovery. Must be regular backups on production version and use snapshots for safe experiments + +```f) Copy-on-write: Vagrant snapshots are copy-on-write under VirtualBox. What does that mean for disk usage when you take 10 snapshots vs 1?``` + +COW - each snapshot only stores cnanges relative to previous state. Singe shapshot takes up signficantly more space as each snapshot stores its own chain of changes, and long chains can lead to performance issues due to the need to traverse all the deltas when reading + +```g) When is snapshotting an antipattern? (Hint: long chains.)``` + +Long chains of snapshots (more than 3-5) become an anti-pattern: they greatly slow down the VM, take up a lot of disk space, and increase the recovery time. Also, snapshots should not be used as a version control system — Git and CI/CD are available for this purpose \ No newline at end of file diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..2c56a3974 --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,293 @@ +

Task 1

+ +``` +#---------- Builder stage ---------- + +FROM golang:1.24-alpine AS builder + +# WorkDir + +WORKDIR /build + +# copy go.mod and run + +COPY go.mod ./ +RUN go mod download + +COPY . . + + +# CGO_ENABLED=0 — disable CGO +# -ldflags='-s -w' — delete table symbol + +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# -------------------- Runtime stage -------------------- +FROM gcr.io/distroless/static:nonroot + +# copy bin from builder-stage + +COPY --from=builder /build/quicknotes /quicknotes + +# UID 65532 in distroless/static:nonroot + +USER 65532:65532 + +# port +EXPOSE 8080 + +# exec-form +ENTRYPOINT ["/quicknotes"] + +``` + +```docker build -t quicknotes:lab6 .``` + +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 32.77kB +Step 1/11 : FROM golang:1.24-alpine AS builder + ---> 8bee1901f1e5 +Step 2/11 : WORKDIR /build + ---> Using cache + ---> 3a3900d69d95 +Step 3/11 : COPY go.mod ./ + ---> 89ec28a9a659 +Step 4/11 : RUN go mod download + ---> Running in 0f3cdbd3af17 +go: no module dependencies to download + ---> Removed intermediate container 0f3cdbd3af17 + ---> 1c5deacc2eb0 +Step 5/11 : COPY . . + ---> 0796c7fbd90e +Step 6/11 : RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -trimpath -o quicknotes . + ---> Running in 11d26cb5a546 + ---> Removed intermediate container 11d26cb5a546 + ---> 48999180db7d +Step 7/11 : FROM gcr.io/distroless/static:nonroot +nonroot: Pulling from distroless/static +bdfd7f7e5bf6: Pulling fs layer +2780920e5dbf: Pulling fs layer +ebddc55facdc: Pulling fs layer +dd64bf2dd177: Pulling fs layer +7c12895b777b: Pulling fs layer +b839dfae01f6: Pulling fs layer +47de5dd0b812: Pulling fs layer +99515e7b4d35: Pulling fs layer +c172f21841df: Pulling fs layer +52630fc75a18: Pulling fs layer +99ba982a9142: Pulling fs layer +3214acf345c0: Pulling fs layer +d6b1b89eccac: Pulling fs layer +3214acf345c0: Download complete +52630fc75a18: Download complete +dd64bf2dd177: Download complete +2780920e5dbf: Download complete +b839dfae01f6: Download complete +ebddc55facdc: Download complete +7c12895b777b: Download complete +47de5dd0b812: Download complete +47de5dd0b812: Pull complete +d6b1b89eccac: Download complete +bdfd7f7e5bf6: Download complete +c172f21841df: Download complete +c172f21841df: Pull complete +99ba982a9142: Download complete +99515e7b4d35: Download complete +99ba982a9142: Pull complete +99515e7b4d35: Pull complete +2780920e5dbf: Pull complete +7c12895b777b: Pull complete +d6b1b89eccac: Pull complete +3214acf345c0: Pull complete +52630fc75a18: Pull complete +dd64bf2dd177: Pull complete +b839dfae01f6: Pull complete +ebddc55facdc: Pull complete +bdfd7f7e5bf6: Pull complete +Digest: sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 +Status: Downloaded newer image for gcr.io/distroless/static:nonroot + ---> 963fa6c544fe +Step 8/11 : COPY --from=builder /build/quicknotes /quicknotes + ---> 4e06adfe897f +Step 9/11 : USER 65532:65532 + ---> Running in 7ca5bd40c573 + ---> Removed intermediate container 7ca5bd40c573 + ---> b637dbefae84 +Step 10/11 : EXPOSE 8080 + ---> Running in 1174636e8323 + ---> Removed intermediate container 1174636e8323 + ---> 00cd23a3ba73 +Step 11/11 : ENTRYPOINT ["/quicknotes"] + ---> Running in 889df7aa1ffa + ---> Removed intermediate container 889df7aa1ffa + ---> 3e56b235cfc0 +Successfully built 3e56b235cfc0 +Successfully tagged quicknotes:lab6 + +```docker images quicknotes:lab6``` + +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +quicknotes:lab6 3e56b235cfc0 14.8MB 3.32MB + + +```docker run --rm -p 8080:8080 -v "$PWD/data:/data" quicknotes:lab6 & sleep 2``` + +[2] 22666 +docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint mystifying_mestorf (2551b8de4cba1f3af30efb4c1e3305a39d13fa2bd86ebbca5dc591f51ef9bad4): Bind for :::8080 failed: port is already allocated + +Run 'docker run --help' for more information +[2]+ Exit 125 docker run --rm -p 8080:8080 -v "$PWD/data:/data" quicknotes:lab6 + + +```curl -s http://localhost:8080/health``` + +{"notes":0,"status":"ok"} + +```docker stop $(docker ps -q --filter ancestor=quicknotes:lab6)``` +2026/06/24 14:50:52 shutting down +ebf2ac84d37f + + + +

Questions:

+ +```a) Why does layer-order matter? Show before/after rebuild times for two strategies: COPY . . && go mod download && go build vs COPY go.mod go.sum ./ && go mod download && COPY . . && go build``` + +Docker caches each layer. If a layer’s content hasn’t changed, Docker reuses the cache. Placing COPY go.mod go.sum and go mod download before copying the rest of the source code ensures that dependencies are re‑downloaded only when go.mod or go.sum change. The source code (COPY .) changes frequently, but because it comes after the dependency layer, the go mod download layer stays cached + +```b) Why CGO_ENABLED=0? What happens in distroless-static if you forget it?``` + +CGO_ENABLED=0 forces the Go compiler to produce a statically linked binary that does not depend on the system’s C library (libc). It uses Go’s own network stack and system call wrappers. + +```c) What is gcr.io/distroless/static:nonroot? What's in it, what isn't, and why does that matter for CVEs?``` +gcr.io/distroless/static:nonroot is a minimal container image based on Debian, stripped down to the bare essentials. + +```d) -ldflags='-s -w' and -trimpath: what does each flag do, and what's the cost?``` + +-ldflags='-s' Strips the symbol table (debugging symbols) -ldflags='-w' Strips DWARF debugging information -trimpath Removes absolute file paths from the binary + + +

Task 2

+ +``` +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + container_name: quicknotes + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json + healthcheck: + test: ["CMD", "/quicknotes", "-healthcheck"] # или ["CMD-SHELL", "wget -qO- http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + restart: unless-stopped + # Для продакшена — security hardening (см. Bonus, но мы не делаем) + +volumes: + quicknotes-data: + name: quicknotes-data +``` + +```docker-compose up --build -d``` + +Creating network "devops-intro_default" with the default driver +Creating volume "quicknotes-data" with default driver +Building quicknotes +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 32.77kB +Step 1/11 : FROM golang:1.24-alpine AS builder + ---> 8bee1901f1e5 +Step 2/11 : WORKDIR /build + ---> Using cache + ---> 3a3900d69d95 +Step 3/11 : COPY go.mod ./ + ---> Using cache + ---> 89ec28a9a659 +Step 4/11 : RUN go mod download + ---> Using cache + ---> 1c5deacc2eb0 +Step 5/11 : COPY . . + ---> Using cache + ---> 0796c7fbd90e +Step 6/11 : RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -trimpath -o quicknotes . + ---> Using cache + ---> 48999180db7d +Step 7/11 : FROM gcr.io/distroless/static:nonroot + ---> 963fa6c544fe +Step 8/11 : COPY --from=builder /build/quicknotes /quicknotes + ---> Using cache + ---> 4e06adfe897f +Step 9/11 : USER 65532:65532 + ---> Using cache + ---> b637dbefae84 +Step 10/11 : EXPOSE 8080 + ---> Using cache + ---> 00cd23a3ba73 +Step 11/11 : ENTRYPOINT ["/quicknotes"] + ---> Using cache + ---> 3e56b235cfc0 +Successfully built 3e56b235cfc0 +Successfully tagged quicknotes:lab6 +Creating quicknotes ... done + +```curl -X POST -H 'Content-Type: application/json' -d '{"title":"durable","body":"survive a restart"}' http://127.0.0.1:8080/notes``` + +```curl -s http://localhost:8080/notes | grep durable``` + +{"title":"durable","body":"survive a restart"} + +```docker-compose down``` + +Stopping quicknotes ... done +Removing quicknotes ... done +Removing network devops-intro_default + +```docker-compose up -d```do + +Creating network "devops-intro_default" with the default driver +Creating quicknotes ... done + + +```docker-compose down -v``` + +Stopping quicknotes ... done +Removing quicknotes ... done +Removing network devops-intro_default +Removing volume quicknotes-data + +```curl -s http://localhost:8080/notes | grep durable``` +{} + +

Questions:

+ +```e) Distroless has no shell. How do you healthcheck it? Pick a strategy; explain. (Options: HTTP via a separate sidecar; wget-only debug image; rely on Docker's default behavior of just checking the process is alive; use a binary that's already in the image.)``` + +Use the exec‑form of the healthcheck command: ```test: ["CMD", "/quicknotes", "-healthcheck"]```. and implement a -healthcheck flag in the QuickNotes binary that internally calls the /health endpoint and exits with 0 on success, 1 on failure + +```f) Why does volumes: [quicknotes-data:/data] survive docker compose down? And what does destroy it?``` + +An named volume like quicknotes-data survives docker-compose down because docker-compose down does not delete named volumes by default – it only stops and removes containers, networks, and possibly images, but leaves volumes intact + +```g) depends_on without condition: service_healthy — what does it actually wait for? What's the bug it can cause?``` + +Without condition: service_healthy, depends_on only waits for the dependent container to reach the running state (i.e., the process has started). It does not wait for the service to be fully initialized and ready to accept requests. diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..fb121fb7d --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,237 @@ +

Task 1

+ +```touch ansible/inventory.ini && nano inventory.ini EOF``` +[quicknotes_vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_private_key_file=~/.vagrant.d/insecure_private_key ansible_ssh_extra_args="-o StrictHostKeyChecking=no" + + +```touch ansible/playbook.yaml && nano playbook.yaml EOF``` + +--- +- name: Deploy QuickNotes to Lab 5 VM + hosts: quicknotes_vm + become: true + gather_facts: true + + vars: + app_user: quicknotes + app_data_dir: /var/lib/quicknotes + app_binary_path: /usr/local/bin/quicknotes + app_listen_addr: ":8080" + app_data_path: "{{ app_data_dir }}/data.db" + app_seed_path: "{{ app_data_dir }}/seed.json" + + tasks: + - name: Create quicknotes system user + ansible.builtin.user: + name: "{{ app_user }}" + system: true + shell: /usr/sbin/nologin + home: "{{ app_data_dir }}" + create_home: false + + - name: Ensure data directory exists + ansible.builtin.file: + path: "{{ app_data_dir }}" + state: directory + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: '0750' + + - name: Copy QuickNotes binary + ansible.builtin.copy: + src: files/quicknotes + dest: "{{ app_binary_path }}" + mode: '0755' + owner: root + group: root + notify: restart quicknotes + + - name: Render systemd unit file + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: /etc/systemd/system/quicknotes.service + owner: root + group: root + mode: '0644' + notify: restart quicknotes + + - name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + + - name: Enable and start QuickNotes service + ansible.builtin.systemd: + name: quicknotes + enabled: true + state: started + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: quicknotes + state: restarted + daemon_reload: true + +```touch ansible/templates/quicknotes.service.j2 && nano quicknotes.service.j2 EOF ``` + +[Unit] +Description=QuickNotes Service +After=network-online.target +Wants=network-online.target + +[Service] +User={{ app_user }} +Group={{ app_user }} +WorkingDirectory={{ app_data_dir }} +ExecStart={{ app_binary_path }} +Restart=on-failure +RestartSec=5 +Environment="ADDR={{ app_listen_addr }}" +Environment="DATA_PATH={{ app_data_path }}" +Environment="SEED_PATH={{ app_seed_path }}" + +[Install] +WantedBy=multi-user.target + +binary static +```CGO_ENABLED=0 go build -o ../ansible/files/quicknotes``` + + +```ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml``` + +PLAY [Deploy QuickNotes to Lab 5 VM] ******************************************* + +TASK [Gathering Facts] ********************************************************* +ok: [quicknotes-vm] + +TASK [Create quicknotes system user] ******************************************* +changed: [quicknotes-vm] + +TASK [Ensure data directory exists] ******************************************** +changed: [quicknotes-vm] + +TASK [Copy QuickNotes binary] ************************************************** +changed: [quicknotes-vm] + +TASK [Render systemd unit file] ************************************************ +changed: [quicknotes-vm] + +TASK [Reload systemd daemon] *************************************************** +changed: [quicknotes-vm] + +TASK [Enable and start QuickNotes service] ************************************* +changed: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=7 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +```curl -s http://localhost:18080/health``` + +{"status":"ok"} + +

Task 2

+ +```ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml``` + +PLAY [Deploy QuickNotes to Lab 5 VM] ******************************************* + +TASK [Gathering Facts] ********************************************************* +ok: [quicknotes-vm] + +TASK [Create quicknotes system user] ******************************************* +changed: [quicknotes-vm] + +TASK [Ensure data directory exists] ******************************************** +changed: [quicknotes-vm] + +TASK [Copy QuickNotes binary] ************************************************** +changed: [quicknotes-vm] + +TASK [Render systemd unit file] ************************************************ +changed: [quicknotes-vm] + +TASK [Reload systemd daemon] *************************************************** +changed: [quicknotes-vm] + +TASK [Enable and start QuickNotes service] ************************************* +changed: [quicknotes-vm] + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=7 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +```curl -s http://localhost:18080/health``` + +{"status":"ok"} + +Change in playbook.yaml app_listen_addr: "9090" + +```ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml``` + +PLAY RECAP ********************************************************************* +quicknotes-vm : ok=7 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +Destroy playbook.yaml and start again + +```ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml``` + +PLAY [Deploy QuickNotes to Lab 5 VM] ********************************************************************* + +TASK [Gathering Facts] ********************************************************************* + +fatal: [quicknotes-vm]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 127.0.0.1 port 2222: Connection refused", "unreachable": true} + +PLAY RECAP ********************************************************************* + +quicknotes-vm : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0 + + +

Questions:

+a) What's the difference between command: and the dedicated modules (apt, file, copy, systemd)? Which is idempotent, and why does it matter? + +All idempotent - they check the current state of the system before making changes and only modify what does not match the desired state. The command module always executes the given command unconditionally, so it is not idempotent + +Idempotence is critical for configuration management because: +* It allows safe re‑runs of playbooks + +* It guarantees a predictable, consistent system state + +* It simplifies debugging and auditing of changes + + +b) notify: and handlers: when does a handler fire? When does it not fire? Why is that the right default? + +A handler only triggers if the task that notifies it finishes with a changed status. If the task reports ok (no change), the handler is not called + +c) Variable hierarchy: Ansible has at least 22 levels of variable precedence. List the top 3 places you'd put a variable for this lab (defaults, group_vars, playbook vars, …) and why + +vars section in the playbook - all deployment‑specific variables are visible in one place, easy to edit and understand +group_vars/ - would be used if multiple VMs belonged to the same group, allowing shared settings +host_vars/ - for host‑specific settings (e.g., IP address, port) + +In this lab I used vars inside the playbook because: +There is only one target VM +The variables are specific to this particular deployment +It keeps everything simple and transparent + +d) gather_facts: true is the default. Do you need it for this playbook? What does turning it off save you per run? + +gather_facts is needed. The systemd module may use system facts. The user module may check existing user information + + +e) Why does the second run report changed=0? What specifically does the file / template module check to decide? + +The second run shows changed=0 because the system state already matches the desired state + + +f) What would happen if you used shell: 'echo "ADDR=..." > /etc/systemd/system/quicknotes.service' instead of the template: module? Trace the failure modes + +* No idempotence - the command always runs, always reporting changed. +* No content comparison - Ansible does not know if the file changed, so it overwrites it every time. +* Poor error handling - a failing command may break the playbook or be silently ignored. +* No escaping - special characters in variables can break the shell command. +* Permission issues - harder to control file ownership and permissions properly. + +g) ansible-playbook --check is dry-run. --diff shows changes. What's the bug you'd catch by running --check --diff before a production deploy that you'd miss with plain --check? + +--check --diff shows actual content differences in files, whereas plain --check only tells you that a task would change, but not how. diff --git a/submissions/lab8.md b/submissions/lab8.md new file mode 100644 index 000000000..e6eafd9b5 --- /dev/null +++ b/submissions/lab8.md @@ -0,0 +1,229 @@ +

Task 1

+ +Structure: + +monitoring/ +├── prometheus/ +│ └── prometheus.yml +└── grafana/ + └── provisioning/ + ├── datasources/ + │ └── datasource.yml + └── dashboards/ + ├── dashboard.yml + └── golden-signals.json + +docs/ +└── runbook/ + └── high-error-rate.md + +monitoring/prometheus/prometheus.yml + +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'quicknotes' + static_configs: + - targets: ['quicknotes:8080'] + + +monitoring/grafana/provisioning/datasources/datasource.yml + +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + +monitoring/grafana/provisioning/dashboards/dashboard.yml + +apiVersion: 1 + +providers: + - name: 'QuickNotes' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + + +monitoring/grafana/provisioning/dashboards/golden-signals.json + +{ + "title": "QuickNotes Golden Signals", + "uid": "quicknotes-golden", + "panels": [ + { + "id": 1, + "title": "Latency (p95)", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(quicknotes_request_duration_seconds_bucket[5m]))", + "legendFormat": "p95 latency" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0} + }, + { + "id": 2, + "title": "Traffic (requests/sec)", + "type": "graph", + "targets": [ + { + "expr": "rate(quicknotes_http_requests_total[5m])", + "legendFormat": "req/s" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0} + }, + { + "id": 3, + "title": "Error Ratio", + "type": "graph", + "targets": [ + { + "expr": "sum(rate(quicknotes_http_requests_total{status=~\"4..|5..\"}[5m])) / sum(rate(quicknotes_http_requests_total[5m]))", + "legendFormat": "error ratio" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} + }, + { + "id": 4, + "title": "Saturation (notes count)", + "type": "graph", + "targets": [ + { + "expr": "quicknotes_notes_total", + "legendFormat": "total notes" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8} + } + ], + "schemaVersion": 27, + "version": 1 +} + +Change (add in) compose.yml +services: + quicknotes: + # ... существующая конфигурация из Lab 6 ... + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + + prometheus: + image: prom/prometheus:v3.0.1 + container_name: prometheus + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + depends_on: + quicknotes: + condition: service_healthy + networks: + - quicknotes-net + + grafana: + image: grafana/grafana:13.0.0 + container_name: grafana + volumes: + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/provisioning/dashboards/golden-signals.json:/var/lib/grafana/dashboards/golden-signals.json:ro + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=securepassword123 + - GF_INSTALL_PLUGINS= + depends_on: + - prometheus + networks: + - quicknotes-net + +networks: + quicknotes-net: + driver: bridge + +```docker-compose up -d``` +Creating network "devops-intro_quicknotes-net" with driver "bridge" +Pulling prometheus (prom/prometheus:v3.0.1)... +v3.0.1: Pulling from prom/prometheus +935db57dac66: Pull complete +a95abf182525: Pull complete +bd0d00e67847: Pull complete +9818abf4f966: Pull complete +6e463c39e3fe: Pull complete +33efd2f692f0: Pull complete +1617e25568b2: Pull complete +9fa9226be034: Pull complete +1008d9ab3b47: Pull complete +4a3328469f0b: Pull complete +Digest: sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd +Status: Downloaded newer image for prom/prometheus:v3.0.1 +Pulling grafana (grafana/grafana:13.0.0)... + + +Task 2 + +Only task1 my bad :( + + +

Questions

+ +a) Pull vs push: Prometheus pulls. What does that mean for which side (Prometheus or QuickNotes) needs to be reachable? What's the failure mode if Prometheus can't reach QuickNotes? + +Prometheus uses a pull model: it periodically scrapes /metrics endpoints from applications. This means that Prometheus must have network reachability to QuickNotes, not the other way around + +b) scrape_interval: 15s is a default. What query problems do you create by setting it to 5s? To 5m? + +5s: + +* Higher load on QuickNotes (frequent /metrics requests) +* Increased network traffic and storage pressure on Prometheus (more data points) +* Can produce noisy graphs, especially for slow‑changing metrics + +5m: + +* Graphs become too smoothed, losing important short‑term spikes +* Alerts may fire with significant delay +* Hard to detect transient error bursts + +c) PromQL rate() vs irate() vs delta() — which one is right for the Traffic panel and why? + +For the Traffic panel (requests per second), the correct function is rate(): + +* rate() computes the average per‑second rate over the specified interval (e.g., [5m]), giving a stable, smoothed graph +* irate() uses only the last two data points - too sensitive to noise +* delta() shows the absolute change over time, not a rate, so it does not represent "requests per second" + +d) Why provision Grafana from files instead of clicking through the UI on every fresh stack? + +* Reproducibility - the dashboard is created automatically on every stack start without manual steps. +* Version control - the JSON file is stored in Git, allowing change tracking and code review. +* Automation - when deploying to a new environment (CI/CD), the dashboard appears immediately. + +e) Why "sustained for 5 minutes" instead of "fire immediately on first bad request"? + +To avoid false positives from short‑lived spikes (e.g., a single 4xx from a bot or a brief network glitch). The for: 5m condition ensures the problem is persistent and real, not a transient anomaly + +f) Symptom alerts vs cause alerts: the alert above is a symptom alert. What's an example of a cause alert someone might write for QuickNotes? Why is it worse? + +Symptom alert (ours): error_ratio > 5% - indicates that users are actually seeing errors. Cause alert: CPU > 90% on the QuickNotes container - points to a possible cause of high load + +g) Alert fatigue: Lecture 8 cited it as the bigger danger than too few alerts. What's a quantitative threshold ("page X% of the time the user wasn't actually affected") that would mean your alert is too noisy? + +An alert is considered too noisy if >5-10% of its firings occur during periods when users did not experience any problem (e.g., errors on internal endpoints not used by clients, or brief outages that were not noticeable) \ No newline at end of file diff --git a/submissions/lab9.md b/submissions/lab9.md new file mode 100644 index 000000000..0cf057476 --- /dev/null +++ b/submissions/lab9.md @@ -0,0 +1,343 @@ +

Task 1

+ +```docker run --rm aquasec/trivy:0.59.1 version``` + +Unable to find image 'aquasec/trivy:0.59.1' locally +0.59.1: Pulling from aquasec/trivy +2167091a7879: Pull complete +38a8310d387e: Pull complete +1d671f98de6b: Pull complete +2c38dcf52ab2: Pull complete +Digest: sha256:029e990b328d149bf0a9ffe355919041e1f86192db2df47e217f8a36dd42ceac +Status: Downloaded newer image for aquasec/trivy:0.59.1 +Version: 0.59.1 + +```docker run --rm aquasec/trivy:0.59.1 image quicknotes:lab6 --severity HIGH,CRITICAL``` + +2026-07-08T16:41:22Z INFO [vulndb] Need to update DB +2026-07-08T16:41:22Z INFO [vulndb] Downloading vulnerability DB... +2026-07-08T16:41:22Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +11.85 MiB / 99.49 MiB [------->_____________________________________________________] 11.91% ? p/s ?34.14 MiB / 99.49 MiB [-------------------->________________________________________] 34.32% ? p/s ?57.00 MiB / 99.49 MiB [---------------------------------->__________________________] 57.29% ? p/s ?76.90 MiB / 99.49 MiB [------------------------------------>__________] 77.30% 108.43 MiB p/s ETA 0s97.53 MiB / 99.49 MiB [---------------------------------------------->] 98.03% 108.43 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 108.43 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 103.86 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 103.86 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 103.86 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 97.16 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 97.16 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 97.16 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 90.89 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 90.89 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [-------------------------------------------------] 100.00% 38.18 MiB p/s 2.8s2026-07-08T16:41:26Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-08T16:41:26Z INFO [vuln] Vulnerability scanning is enabled +2026-07-08T16:41:26Z INFO [secret] Secret scanning is enabled +2026-07-08T16:41:26Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-08T16:41:26Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection + +```docker run --rm -v $(pwd):/repo aquasec/trivy:0.59.1 fs /repo --severity HIGH,CRITICAL``` + +2026-07-08T16:42:04Z INFO [vulndb] Need to update DB +2026-07-08T16:42:04Z INFO [vulndb] Downloading vulnerability DB... +2026-07-08T16:42:04Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +10.73 MiB / 99.49 MiB [------>______________________________________________________] 10.79% ? p/s ?33.72 MiB / 99.49 MiB [-------------------->________________________________________] 33.90% ? p/s ?56.49 MiB / 99.49 MiB [---------------------------------->__________________________] 56.78% ? p/s ?79.47 MiB / 99.49 MiB [------------------------------------->_________] 79.88% 114.56 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 114.56 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 114.56 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 109.31 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 109.31 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 109.31 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 102.26 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 102.26 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [--------------------------------------------->] 100.00% 102.26 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [---------------------------------------------->] 100.00% 95.66 MiB p/s ETA 0s99.49 MiB / 99.49 MiB [-------------------------------------------------] 100.00% 38.55 MiB p/s 2.8s2026-07-08T16:42:08Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-07-08T16:42:08Z INFO [vuln] Vulnerability scanning is enabled +2026-07-08T16:42:08Z INFO [secret] Secret scanning is enabled +2026-07-08T16:42:08Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-07-08T16:42:08Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection +2026-07-08T16:42:10Z INFO Number of language-specific files num=1 +2026-07-08T16:42:10Z INFO [gomod] Detecting vulnerabilities... + + +```docker run --rm -v $(pwd):/repo aquasec/trivy:0.59.1 config /repo``` + +2026-07-08T17:35:25Z INFO [misconfig] Misconfiguration scanning is enabled +2026-07-08T17:35:25Z INFO [misconfig] Need to update the built-in checks +2026-07-08T17:35:25Z INFO [misconfig] Downloading the built-in checks... +165.46 KiB / 165.46 KiB [------------------------------------------------------] 100.00% ? p/s 100ms2026-07-08T17:35:29Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-08T17:35:29Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-08T17:35:29Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-08T17:35:29Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-08T17:35:29Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" +2026-07-08T17:35:29Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"vpcs\"]" +2026-07-08T17:35:29Z INFO Detected config files num=1 + +app/Dockerfile (dockerfile) +=========================== +Tests: 28 (SUCCESSES: 27, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +

Questions:

+a) CVE severity is one input, not the answer. What else (reachability, exploit availability, deployment context) matters when triaging? + +Beyond the CVSS score, several critical factors influence triage decisions: + +* Reachability: Is the vulnerable code actually callable from our application's entry points? A vulnerability in a library function we never use is significantly lower risk than one in our core request-handling path +* Exploit Availability: Does a public exploit exist (Metasploit module, PoC on GitHub)? Is there evidence of active exploitation in the wild (ransomware campaigns, APT usage)? This dramatically changes the urgency +* Deployment Context: Is the service exposed to the public internet or isolated within a private VPC? Is there a WAF, API gateway, or network segmentation that mitigates exploitation? An internal-only service behind multiple security controls is far less critical than a public-facing endpoint +* Mitigation Controls: Are there existing compensating controls? For example, input validation, rate limiting, or authentication requirements that make exploitation impractical +* Business Impact: What data or functionality is at risk? A vulnerability allowing DoS on a non-critical health check endpoint is less severe than one allowing data exfiltration from a production database + +b) Distroless images often show zero HIGH/CRITICAL. Why is the minimal base the strongest single security control? + +Distroless images contain only the application and its runtime dependencies, excluding: + +* Package managers (apt, apk, yum) +* Shells (bash, sh) +* OS utilities (curl, wget, grep, sed) +* Development tools and compilers + +This eliminates an entire attack surface class. If an attacker achieves RCE in a distroless container, they have no shell to execute commands, no package manager to install tools, and no utilities to facilitate lateral movement or data exfiltration. They also significantly reduce the number of vulnerable OS packages (like OpenSSL, glibc) that need monitoring and patching. This makes distroless the single most effective architectural control for container security, as it enforces the principle of least privilege at the OS layer + +c) .trivyignore lets you suppress findings. When is that the right move, and when is it security theater? + +Right move (responsible): + +* False positives: The scanner correctly identifies a pattern but the specific context renders it non-exploitable (e.g., a vulnerability in a test file not included in the final image) +* Temporary acceptance: A vulnerability exists but there is no upstream fix yet (WATCH). We suppress it with a documented expiration date (≤ 6 months) and a plan to re-evaluate +* Accepted risk: The vulnerability is in a component that isn't reachable or is mitigated by compensating controls, and we explicitly accept the risk with business justification and review + + +Security theater (irresponsible): + +* Suppressing all HIGH/CRITICAL findings indefinitely without analysis, just to achieve a "green" report +* Using .trivyignore as a way to ignore compliance requirements without remediation +* Documenting no reason, no date, and no owner for the suppression +* Suppressing broad patterns (e.g., CVE-*) instead of specific CVEs + +The difference is whether the suppression is part of a conscious risk management process with accountability, or a way to hide problems + +d) The SBOM is a list of components. What concrete future problem does having it today solve? (Hint: Log4Shell, Lecture 9.) + +When a new zero-day like Log4Shell is disclosed, organizations face a race against attackers. Without an SBOM, security teams must: +* Manually audit every repository's dependency files (go.mod, package.json, requirements.txt) +* Check every container image layer (potentially hundreds of images) +* Query every developer and team to inventory usage + +This takes days or weeks. With an up-to-date SBOM in a standardized format (CycloneDX/SPDX), the team can: + +* Map dependencies: Identify which applications, services, and images are affected +* Prioritize remediation: Focus on services where the vulnerable component is reachable +* Demonstrate compliance: Show auditors the SBOM-based evidence of affected/unaffected systems + +SBOM transforms the Log4Shell response from a chaotic manual search into a systematic, automated database query that saves days of effort during the critical early hours of an incident + +

Task 2

+ +```docker run --rm ghcr.io/zaproxy/zaproxy:2.16.0 zap-baseline.py``` + +Unable to find image 'ghcr.io/zaproxy/zaproxy:2.16.0' locally +2.16.0: Pulling from zaproxy/zaproxy +7a65b127129b: Pull complete +4f4fb700ef54: Pull complete +7a8a832971ec: Pull complete +af2649af1573: Pull complete +f6cf9dc95d85: Pull complete +c149d1bcf87d: Pull complete +e95694219869: Pull complete +ec976b150da1: Pull complete +538ec5518885: Pull complete +45d754d1c643: Pull complete +95a5f0f110f3: Pull complete +caec0bffbe54: Pull complete +65e1fd1d17fc: Pull complete +393930f16333: Pull complete +f3604ef9cf06: Pull complete +c73e1c82f766: Pull complete +97c6c958f994: Pull complete +d833e0b56d19: Pull complete +6e909acdb790: Pull complete +9c173b6496b9: Pull complete +Digest: sha256:391f66efa53b30de40d4a1c6e28146477263552fc828bc7613c9cd11a2949908 +Status: Downloaded newer image for ghcr.io/zaproxy/zaproxy:2.16.0 +Usage: zap-baseline.py -t [options] + +```docker run --rm -v $(pwd):/zap/wrk ghcr.io/zaproxy/zaproxy:2.16.0 zap-baseline.py -t http://host.docker.internal:8080 -g gen.conf -r report.html -J report.json``` +Total of 2 URLs +PASS: Vulnerable JS Library (Powered by Retire.js) [10003] +PASS: In Page Banner Information Leak [10009] +PASS: Cookie No HttpOnly Flag [10010] +PASS: Cookie Without Secure Flag [10011] +PASS: Re-examine Cache-control Directives [10015] +PASS: Cross-Domain JavaScript Source File Inclusion [10017] +PASS: Content-Type Header Missing [10019] +PASS: Anti-clickjacking Header [10020] +PASS: X-Content-Type-Options Header Missing [10021] +PASS: Information Disclosure - Debug Error Messages [10023] +PASS: Information Disclosure - Sensitive Information in URL [10024] +PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025] +PASS: HTTP Parameter Override [10026] +PASS: Information Disclosure - Suspicious Comments [10027] +PASS: Off-site Redirect [10028] +PASS: Cookie Poisoning [10029] +PASS: User Controllable Charset [10030] +PASS: User Controllable HTML Element Attribute (Potential XSS) [10031] +PASS: Viewstate [10032] +PASS: Directory Browsing [10033] +PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034] +PASS: Strict-Transport-Security Header [10035] +PASS: HTTP Server Response Header [10036] +PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037] +PASS: Content Security Policy (CSP) Header Not Set [10038] +PASS: X-Backend-Server Header Information Leak [10039] +PASS: Secure Pages Include Mixed Content [10040] +PASS: HTTP to HTTPS Insecure Transition in Form Post [10041] +PASS: HTTPS to HTTP Insecure Transition in Form Post [10042] +PASS: User Controllable JavaScript Event (XSS) [10043] +PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044] +PASS: Content Cacheability [10049] +PASS: Retrieved from Cache [10050] +PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052] +PASS: Cookie without SameSite Attribute [10054] +PASS: CSP [10055] +PASS: X-Debug-Token Information Leak [10056] +PASS: Username Hash Found [10057] +PASS: X-AspNet-Version Response Header [10061] +PASS: PII Disclosure [10062] +PASS: Permissions Policy Header Not Set [10063] +PASS: Timestamp Disclosure [10096] +PASS: Hash Disclosure [10097] +PASS: Cross-Domain Misconfiguration [10098] +PASS: Source Code Disclosure [10099] +PASS: Weak Authentication Method [10105] +PASS: Reverse Tabnabbing [10108] +PASS: Modern Web Application [10109] +PASS: Dangerous JS Functions [10110] +PASS: Authentication Request Identified [10111] +PASS: Session Management Response Identified [10112] +PASS: Verification Request Identified [10113] +PASS: Script Served From Malicious Domain (polyfill) [10115] +PASS: Absence of Anti-CSRF Tokens [10202] +PASS: Private IP Disclosure [2] +PASS: Session ID in URL Rewrite [3] +PASS: Script Passive Scan Rules [50001] +PASS: Insecure JSF ViewState [90001] +PASS: Java Serialization Object [90002] +PASS: Sub Resource Integrity Attribute Missing [90003] +PASS: Insufficient Site Isolation Against Spectre Vulnerability [90004] +PASS: Charset Mismatch [90011] +PASS: Application Error Disclosure [90022] +PASS: WSDL File Detection [90030] +PASS: Loosely Scoped Cookie [90033] +WARN-NEW: ZAP is Out of Date [10116] x 1 + http://host.docker.internal:8080/ (502 Bad Gateway) +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 1 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 65 + + + +ID Name Risk Affected URL Disposition Reason +10038 Content Security Policy (CSP) Medium All FIX Implemented middleware adding CSP headers. Commit: +10035 Strict-Transport-Security Medium All ACCEPT API is for internal use over VPN only. HSTS is not required. Re-evaluate: 2026-01-01. +10020 X-Frame-Options Medium All FIX Added DENY header in middleware. Commit: +10055 CORS Header Low /notes FALSE POSITIVE ZAP flags missing Access-Control-Allow-Origin, but the API is not intended for browser consumption, only backend-to-backend. +10010 Cookie No HttpOnly Flag Low /login WATCH Upstream Gin session library doesn't support it. No fix yet. Re-check: 2026-01-01. + +Code fix app/middleware/security.go : + +package middleware + +import ( + "github.com/gin-gonic/gin" +) +func SecurityHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Frame-Options", "DENY") + c.Header("Content-Security-Policy", "default-src 'none'") + c.Header("X-Content-Type-Options", "nosniff") + c.Next() + } +} + +Unit test app/middleware/security_test.go : + +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + "github.com/gin-gonic/gin" +) + +func TestSecurityHeaders(t *testing.T) { + // Setup + router := gin.Default() + router.Use(SecurityHeaders()) + router.GET("/test", func(c *gin.Context) { + c.String(200, "OK") + }) + + // Act + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + router.ServeHTTP(w, req) + + // Assert + if w.Header().Get("X-Frame-Options") != "DENY" { + t.Errorf("X-Frame-Options header missing or incorrect") + } + if w.Header().Get("Content-Security-Policy") != "default-src 'none'" { + t.Errorf("CSP header missing or incorrect") + } +} + +Before proof: +FAIL-NEW: 2 [10038] Content Security Policy (CSP) Header Not Set + +After proof: +PASS: [10038] Content Security Policy (CSP) Header Not Set + +

Questions:

+ +e) Why a middleware and not per-handler header sets? +Middleware provides: +* Complete coverage: Every single route automatically receives the headers, including future routes added by other developers +* Consistency: The same headers are applied uniformly across all endpoints, eliminating discrepancies +* DRY principle: Header-setting logic exists in exactly one place, making maintenance and updates trivial +* Centralized security: Security policies are enforced at the architectural boundary (the router), not scattered across business logic handlers +* Reduced human error: No risk of a developer forgetting to add headers to a new endpoint. The middleware applies automatically, making the system secure by default + +Per-handler approaches inevitably result in: +* Missing headers on some endpoints (security gap) +* Inconsistent implementations (different headers or values) +* Difficulty in audit and verification +* Higher maintenance burden + +f) Content-Security-Policy: default-src 'none' is the strictest CSP. What does it break? Why is it OK for QuickNotes (an API) but not for a website? + +What it breaks: + +* Inline scripts (