diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1aaa302..50ef52f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,46 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: + platform-doctor: + name: Platform Doctor + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache: false + + - name: Install Platform Doctor + run: go install github.com/chaosslabs/platform-doctor/cmd/platform-doctor@e8db1a5be9a72fc6aa9fcb5f5e0ae047f49325e1 + + - name: Scan repository + run: platform-doctor scan . --output "$RUNNER_TEMP/platform-doctor-report.md" + + - name: Add report to job summary + if: always() + run: | + if [ -f "$RUNNER_TEMP/platform-doctor-report.md" ]; then + cat "$RUNNER_TEMP/platform-doctor-report.md" >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload Platform Doctor report + if: always() + uses: actions/upload-artifact@v4 + with: + name: platform-doctor-report + path: ${{ runner.temp }}/platform-doctor-report.md + retention-days: 30 + test: name: Test and Lint runs-on: ubuntu-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index bc1396e..da8db13 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -5,14 +5,14 @@ on: branches: [ main ] workflow_dispatch: # Allow manual triggering +permissions: + contents: read + jobs: e2e: name: End-to-End Tests runs-on: ubuntu-latest timeout-minutes: 15 - permissions: - contents: read - issues: write steps: - name: Checkout code @@ -105,8 +105,17 @@ jobs: path: dashboard/playwright-report/ retention-days: 7 - - name: Comment PR on failure - if: failure() && github.event_name == 'push' + report-failure: + name: Report E2E failure + runs-on: ubuntu-latest + needs: e2e + if: failure() && needs.e2e.result == 'failure' && github.event_name == 'push' + permissions: + contents: read + # Required only to create the failure issue; test jobs stay read-only. + issues: write + steps: + - name: Create failure issue uses: actions/github-script@v7 with: script: | diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index 5efaa30..46c7e77 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -12,14 +12,16 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: release: - name: Build and Release + name: Build and Validate runs-on: ubuntu-latest - + outputs: + tag: ${{ steps.tag.outputs.tag_name }} + release_exists: ${{ steps.release_check.outputs.release_exists }} + steps: - name: Checkout code uses: actions/checkout@v4 @@ -222,19 +224,50 @@ jobs: NETKIT_DASHBOARD_BASE_PATH: /netkit NETKIT_DOCKER_IMAGE: netkit:dashboard-prefix-validation + - name: Upload validated release artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: | + dist/* + RELEASE_NOTES.md + if-no-files-found: error + retention-days: 1 + + publish: + name: Publish Release + runs-on: ubuntu-latest + needs: release + permissions: + # Required only to create/update GitHub releases. + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.release.outputs.tag }} + persist-credentials: false + + - name: Download validated release artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Delete existing release (if manual trigger) - if: github.event_name == 'workflow_dispatch' && steps.release_check.outputs.release_exists == 'true' + if: github.event_name == 'workflow_dispatch' && needs.release.outputs.release_exists == 'true' run: | echo "🗑️ Deleting existing release to recreate with updated artifacts..." - gh release delete "${{ steps.tag.outputs.tag_name }}" --yes + gh release delete "${{ needs.release.outputs.tag }}" --yes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: - tag_name: ${{ steps.tag.outputs.tag_name }} - name: "Release ${{ steps.tag.outputs.tag_name }}" + tag_name: ${{ needs.release.outputs.tag }} + name: "Release ${{ needs.release.outputs.tag }}" body_path: RELEASE_NOTES.md draft: false prerelease: false @@ -253,7 +286,7 @@ jobs: - name: Extract version from tag id: docker_meta run: | - VERSION=${{ steps.tag.outputs.tag_name }} + VERSION=${{ needs.release.outputs.tag }} VERSION=${VERSION#v} echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/netkit:${VERSION},${{ secrets.DOCKERHUB_USERNAME }}/netkit:latest" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index de38afb..8b33eae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ dashboard/test-results dashboard/playwrite-report bin/ +# macOS Finder metadata +.DS_Store + # Embedded dashboard files (generated during build) internal/dashboard/out/ - diff --git a/Dockerfile b/Dockerfile index f6c1a30..9512d5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,9 @@ FROM alpine:latest ARG NETKIT_DASHBOARD_BASE_PATH="" ENV NETKIT_DASHBOARD_BASE_PATH=${NETKIT_DASHBOARD_BASE_PATH} -RUN apk --no-cache add ca-certificates +RUN apk --no-cache add ca-certificates \ + && addgroup -S -g 10001 netkit \ + && adduser -S -D -H -u 10001 -G netkit netkit WORKDIR /app @@ -68,5 +70,7 @@ COPY --from=builder /build/netkit . # Expose ports (proxy, admin, dashboard) EXPOSE 8080 8081 3000 +USER 10001:10001 + # Run the proxy server with embedded dashboard CMD ["./netkit", "serve", "--port", "8080", "--admin-port", "8081", "--dashboard", "--dashboard-port", "3000", "--log-level", "info"] diff --git a/README.md b/README.md index fcdb2dc..27a8941 100644 --- a/README.md +++ b/README.md @@ -292,3 +292,8 @@ codes are not captured. CONNECT records leave payload sizes at zero (unmeasured) and omit request headers to avoid retaining proxy credentials. Transfer errors after establishment do not change the setup result. Requests made through the dashboard's destination-header API follow its separate HTTP forwarding path. + +## Maintainer operations + +See [RUNBOOK.md](RUNBOOK.md) for release validation, local troubleshooting, +CLI/container rollback, and the applicability of cost checks to this distributed tool. diff --git a/RUNBOOK.md b/RUNBOOK.md new file mode 100644 index 0000000..910b2eb --- /dev/null +++ b/RUNBOOK.md @@ -0,0 +1,89 @@ +# Netkit maintainer runbook + +Netkit is a CLI and container image distributed to users, not a hosted service. +There is no centrally operated Netkit production deployment or service on-call. +Repository maintainers own release quality; users own their installations, +network exposure, infrastructure, and any upstream services they call. + +## Validate a release + +Run `make test`, `make e2e`, and `make test-docker-dashboard` before publishing. +The Docker check verifies the non-root runtime user, embedded dashboard, +JavaScript assets, and same-origin health endpoint. The tag workflow also checks +an image built with the `/netkit` URL prefix before publication. + +The Release on Tag workflow publishes platform binaries and checksums to GitHub +Releases and images to Docker Hub. Build/test jobs have read-only repository +access. The publishing job needs `contents: write` to publish releases; Docker +Hub uses its own credentials and needs no GitHub `packages: write` permission. +E2E failure reporting alone needs `issues: write`. Platform Doctor currently +flags these explicit job permissions even though they are necessary. + +## Diagnose a local installation + +Start the CLI with `netkit serve --admin-port 8081`, then check: + +```sh +curl --fail http://localhost:8081/healthz +curl --fail http://localhost:3000/api/admin/healthz +``` + +Inspect terminal output (or `docker logs CONTAINER`) for bind errors, upstream +connection failures, and dashboard configuration errors. Confirm the configured +ports and `NETKIT_DASHBOARD_BASE_PATH`. Keep proxy and admin access on trusted +networks. Request history is in memory and disappears when the process restarts; +redact captured credentials and bodies before sharing diagnostic reports. + +## Rollback a CLI or container installation + +Rollback means restoring a user's previous working version, not reverting a +central service deployment. Record the current version, startup arguments, and +previous working version before upgrading. + +For a CLI installed from a release: + +1. Stop the running Netkit process using the same terminal or service manager + that started it. This interrupts proxy connections and clears request history. +2. Download the correct OS/architecture binary and `checksums.txt` from the + previous known-good GitHub release. Verify the binary against its checksum + (`sha256sum` on Linux or `shasum -a 256` on macOS). +3. Replace the installed executable, make it executable on Unix, and restart + with the same arguments. For example, after downloading and verifying the + macOS ARM64 binary, for an installation in `$HOME/.local/bin`: + + ```sh + install -m 755 ./netkit-darwin-arm64 "$HOME/.local/bin/netkit" + "$HOME/.local/bin/netkit" serve --admin-port 8081 + ``` + +For a container, restore the previous image tag or digest in the user's Docker, +Compose, or orchestrator configuration and recreate the container with the same +ports, environment, and arguments. Avoid `latest` when reproducible upgrades and +rollback matter. Check `/healthz`, dashboard loading, and a request to a known +upstream after restarting either installation. + +If a published version is broken, maintainers should document the affected +version and recommend the previous working version, revert or fix the regression +in a pull request, and publish a new patch release. Do not move an existing tag +or overwrite its artifacts as a rollback procedure. + +## Budget and cost alerts: applicability + +Netkit has no hosted-service cloud budget, billing account, or customer spend +limit to configure. Installing the CLI does not provision cloud infrastructure. +Users are responsible for any hosting, network egress, and paid upstream API +charges generated by their own requests. Netkit does not enforce those budgets. + +Repository distribution can still consume GitHub Actions minutes/artifact +storage and Docker Hub allowances. The repository/billing account owner should +review those providers' usage before enabling paid capacity and after unexpected +build activity. No dollar budget or live cost alert is configured by this +repository, and this document does not claim otherwise. If paid usage is enabled, +the account owner must choose a monthly cap and configure provider notifications +at 80% and 100% of that cap. At 80%, review repeated builds and artifact retention; +at 100%, pause discretionary builds until the owner approves further spending. + +This is maintainer guidance, not evidence that external billing settings or +notifications have been enabled. A cloud-budget finding against this CLI should +be treated as a check-applicability limitation, not as a missing production +control. diff --git a/scripts/validate-docker-dashboard.sh b/scripts/validate-docker-dashboard.sh index 488b16e..6301dfa 100755 --- a/scripts/validate-docker-dashboard.sh +++ b/scripts/validate-docker-dashboard.sh @@ -42,6 +42,11 @@ docker run -d \ -p "127.0.0.1:${dashboard_port}:3000" \ "$image" >/dev/null +if [ "$(docker exec "$container" id -u)" != "10001" ]; then + echo "Docker image must run as the non-root netkit user (UID 10001)" >&2 + exit 1 +fi + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do if curl -fsS "$dashboard_url" > "$tmp_dashboard"; then break