diff --git a/.checkov.yaml b/.checkov.yaml new file mode 100644 index 0000000..1b8debd --- /dev/null +++ b/.checkov.yaml @@ -0,0 +1,10 @@ +soft-fail: false +quiet: true +compact: true +framework: + - all +skip-check: + - CKV_K8S_21 # Default namespace usage + - CKV_K8S_43 # Image tag validation (using latest tags intentionally) + - CKV_K8S_14 # Image tag should be fixed (same as above) + - CKV_K8S_22 # Read-only filesystem (IRC apps need to write to volumes) diff --git a/.gitea/workflows/best-practices.yaml b/.gitea/workflows/best-practices.yaml new file mode 100644 index 0000000..1765905 --- /dev/null +++ b/.gitea/workflows/best-practices.yaml @@ -0,0 +1,277 @@ +name: Best Practices + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + kube-score: + name: Kube-score Analysis + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and kube-score + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install kube-score + wget https://github.com/zegl/kube-score/releases/download/v1.18.0/kube-score_1.18.0_linux_amd64.tar.gz + tar -xzf kube-score_1.18.0_linux_amd64.tar.gz + chmod +x kube-score + mv kube-score /usr/local/bin/ + + - name: Run kube-score + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . | kube-score score - \ + --ignore-test pod-networkpolicy \ + --ignore-test deployment-has-poddisruptionbudget \ + --ignore-test container-security-context-readonlyrootfilesystem \ + --ignore-test container-image-tag \ + --ignore-test container-security-context-user-group-id \ + --ignore-test probe-not-identical \ + --output-format ci + fi + + polaris: + name: Polaris Audit + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and polaris + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install polaris + wget https://github.com/FairwindsOps/polaris/releases/download/9.5.0/polaris_linux_amd64.tar.gz + tar -xzf polaris_linux_amd64.tar.gz + chmod +x polaris + mv polaris /usr/local/bin/ + + - name: Run Polaris audit + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . > manifests.yaml + polaris audit --audit-path manifests.yaml \ + --format pretty \ + --set-exit-code-on-danger \ + --set-exit-code-below-score 70 + fi + + resource-analysis: + name: Resource Usage Analysis + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and yq + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq + chmod +x yq + mv yq /usr/local/bin/ + + - name: Analyze resource requests and limits + run: | + echo "# Resource Analysis Report" + echo "" + echo "## Applications Resource Configuration" + echo "" + echo "| Application | Container | CPU Request | CPU Limit | Memory Request | Memory Limit |" + echo "|-------------|-----------|-------------|-----------|----------------|--------------|" + + # Find all directories with kustomization.yaml + find . -maxdepth 2 -name "kustomization.yaml" | while read config; do + app_dir=$(dirname "$config") + if [ "$app_dir" != "." ]; then + manifests=$(kubectl kustomize "$app_dir" 2>/dev/null) + if [ -n "$manifests" ]; then + echo "$manifests" | yq eval-all ' + select(.kind == "Deployment" or .kind == "StatefulSet") | + .spec.template.spec.containers[] | + "| '"$app_dir"' | \(.name) | \(.resources.requests.cpu // "none") | \(.resources.limits.cpu // "none") | \(.resources.requests.memory // "none") | \(.resources.limits.memory // "none") |" + ' - 2>/dev/null || true + fi + fi + done + + pr-summary: + name: PR Summary Report + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + needs: [kube-score, polaris, resource-analysis] + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Generate PR summary + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments + run: | + cat > summary.md << EOF + ## Best Practices Validation Summary + + ✅ All validation checks completed + + ### Checks Run: + - **kube-score**: Kubernetes best practices analysis + - **Polaris**: Security and reliability audit + - **Resource Analysis**: CPU and memory configuration review + + See individual job logs for detailed results. + + --- + *Automated by Gitea Actions* + EOF + + if [ -n "${GITEA_TOKEN}" ]; then + jq -n --rawfile body summary.md '{body: $body}' > comment-payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @comment-payload.json \ + "${GITEA_API}" || echo "Failed to post comment (token may not be configured)" + else + echo "GITEA_TOKEN not configured, skipping comment" + cat summary.md + fi + + polaris-pr-review: + name: Polaris PR Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install tools + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install polaris + wget https://github.com/FairwindsOps/polaris/releases/download/9.5.0/polaris_linux_amd64.tar.gz + tar -xzf polaris_linux_amd64.tar.gz + chmod +x polaris + mv polaris /usr/local/bin/ + + # Install jq + apt-get update && apt-get install -y jq + + - name: Run Polaris and post review + env: + GITEA_TOKEN: ${{ secrets.POLARIS_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews + run: | + if [ ! -f "kustomization.yaml" ]; then + echo "No root kustomization.yaml, skipping Polaris review" + exit 0 + fi + + kubectl kustomize . > manifests.yaml + if [ ! -s manifests.yaml ]; then + echo "Manifests are empty, skipping" + exit 0 + fi + + polaris audit --audit-path manifests.yaml --format json > polaris-results.json || true + + DANGERS=$(jq '.Summary.Dangers // 0' polaris-results.json) + WARNINGS=$(jq '.Summary.Warnings // 0' polaris-results.json) + SCORE=$(jq '.Summary.Score // 0' polaris-results.json) + + if [ "$DANGERS" -gt 0 ]; then + REVIEW_STATE="REQUEST_CHANGES" + VERDICT="BLOCKED: ${DANGERS} danger(s) detected. Score: ${SCORE}%" + EXIT_CODE=1 + elif [ "$WARNINGS" -gt 0 ]; then + REVIEW_STATE="COMMENT" + VERDICT="WARNING: ${WARNINGS} warning(s) detected. Score: ${SCORE}%" + EXIT_CODE=0 + else + REVIEW_STATE="APPROVED" + VERDICT="PASSED: No dangers or warnings. Score: ${SCORE}%" + EXIT_CODE=0 + fi + + DETAILS=$(jq -r ' + .Results[]? | + .Name as $resName | .Kind as $resKind | .Namespace as $resNs | + ( + (.PodResult?.Results[]? | {sev: .Severity, msg: .Message, check: .ID, target: "Pod"}), + (.PodResult?.ContainerResults[]? | .Name as $contName | .Results[]? | {sev: .Severity, msg: .Message, check: .ID, target: $contName}) + ) | + select(.sev == "danger" or .sev == "warning") | + "| \(.sev) | \($resKind)/\($resName) | \(.target) | \(.check) | \(.msg) |" + ' polaris-results.json | head -c 4000) + + cat > review-body.md << INTERNAL_EOF + ## Polaris Audit Results + + **${VERDICT}** + + ### Summary + | Metric | Value | + |--------|-------| + | Score | ${SCORE}% | + | Dangers | ${DANGERS} | + | Warnings | ${WARNINGS} | + +
+ Issues (click to expand) + + | Severity | Resource | Container | Check | Message | + |----------|----------|-----------|-------|---------| + \${DETAILS} + +
+ + --- + *Scanned by [Polaris](https://github.com/FairwindsOps/polaris)* + INTERNAL_EOF + + jq -n \ + --rawfile body review-body.md \ + --arg event "$REVIEW_STATE" \ + '{body: $body, event: $event}' > review-payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @review-payload.json \ + "${GITEA_API}" | jq . + + exit $EXIT_CODE diff --git a/.gitea/workflows/security.yaml b/.gitea/workflows/security.yaml new file mode 100644 index 0000000..4399bda --- /dev/null +++ b/.gitea/workflows/security.yaml @@ -0,0 +1,302 @@ +name: Security Scan + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + trivy-scan: + name: Trivy Security Scan + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + + - name: Run Trivy config scan + run: | + trivy config \ + --severity CRITICAL,HIGH \ + --ignorefile .trivyignore \ + --exit-code 0 \ + --format table \ + . + + trivy-pr-review: + name: Trivy PR Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Trivy and jq + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + apt-get update && apt-get install -y jq + + - name: Get changed files + id: changed + run: | + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.yaml' '*.yml' '*.tf' | tr '\n' ' ') + echo "files=${CHANGED_FILES}" >> $GITHUB_OUTPUT + echo "Changed files: ${CHANGED_FILES}" + + - name: Run Trivy scan and post review + env: + GITEA_TOKEN: ${{ secrets.TRIVY_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews + CHANGED_FILES: ${{ steps.changed.outputs.files }} + run: | + if [ -z "${CHANGED_FILES}" ]; then + echo "No IaC files changed, skipping scan" + exit 0 + fi + + trivy config \ + --severity CRITICAL,HIGH,MEDIUM,LOW \ + --ignorefile .trivyignore \ + --exit-code 0 \ + --format json \ + --output trivy-results.json \ + . + + CHANGED_FILES_JSON=$(echo "${CHANGED_FILES}" | tr ' ' '\n' | sed '/^$/d' | jq -R . | jq -s .) + + CRITICAL=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "CRITICAL")] | length' \ + trivy-results.json) + HIGH=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "HIGH")] | length' \ + trivy-results.json) + MEDIUM=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "MEDIUM")] | length' \ + trivy-results.json) + LOW=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "LOW")] | length' \ + trivy-results.json) + TOTAL_FAILED=$(( CRITICAL + HIGH + MEDIUM + LOW )) + + if [ "$CRITICAL" -gt 0 ]; then + REVIEW_STATE="REQUEST_CHANGES" + VERDICT="BLOCKED: ${CRITICAL} critical finding(s) detected" + EXIT_CODE=1 + elif [ "$HIGH" -gt 0 ]; then + REVIEW_STATE="COMMENT" + VERDICT="WARNING: ${HIGH} high severity finding(s) detected" + EXIT_CODE=0 + else + REVIEW_STATE="APPROVED" + VERDICT="PASSED: No critical or high severity findings" + EXIT_CODE=0 + fi + + DETAILS=$(jq -r --argjson files "$CHANGED_FILES_JSON" ' + [.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | + .Target as $t | .Misconfigurations[]? | + "| \(.Severity) | \(.ID) | \(.Title) | \($t) |" + ] | join("\n")' trivy-results.json) + + cat > review-body.md << EOF + ## Trivy Security Scan Results + + **${VERDICT}** + + > Scanned ${CHANGED_FILES:-"no files"} + + ### Summary + | Severity | Count | + |----------|-------| + | Critical | ${CRITICAL} | + | High | ${HIGH} | + | Medium | ${MEDIUM} | + | Low | ${LOW} | + | **Total** | **${TOTAL_FAILED}** | + +
+ Failed Checks (click to expand) + + | Severity | Check ID | Description | File | + |----------|----------|-------------|------| + ${DETAILS} + +
+ + --- + *Scanned by [Trivy](https://github.com/aquasecurity/trivy)* + EOF + + jq -n \ + --rawfile body review-body.md \ + --arg event "$REVIEW_STATE" \ + '{body: $body, event: $event}' > review-payload.json + + echo "Posting review to: ${GITEA_API}" + echo "Review state: ${REVIEW_STATE}" + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @review-payload.json \ + "${GITEA_API}" || echo "Failed to post review (token may not be configured)" + + exit $EXIT_CODE + + checkov-scan: + name: Checkov IaC Scan + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Checkov + run: | + python3 -m venv /opt/checkov + /opt/checkov/bin/pip install checkov + + - name: Run Checkov scan + run: | + /opt/checkov/bin/checkov -d . \ + --config-file .checkov.yaml \ + --output cli + + checkov-pr-review: + name: Checkov PR Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Checkov and jq + run: | + python3 -m venv /opt/checkov + /opt/checkov/bin/pip install checkov + apt-get update && apt-get install -y jq + + - name: Get changed files + id: changed + run: | + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.yaml' '*.yml' '*.tf' | tr '\n' ' ') + echo "files=${CHANGED_FILES}" >> $GITHUB_OUTPUT + echo "Changed files: ${CHANGED_FILES}" + + - name: Run Checkov scan and post review + env: + GITEA_TOKEN: ${{ secrets.CHECKOV_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews + CHANGED_FILES: ${{ steps.changed.outputs.files }} + run: | + if [ -z "${CHANGED_FILES}" ]; then + echo "No IaC files changed, skipping scan" + exit 0 + fi + + /opt/checkov/bin/checkov -d . \ + --config-file .checkov.yaml \ + --output json \ + > checkov-results.json || true + + CHANGED_FILES_JSON=$(echo "${CHANGED_FILES}" | tr ' ' '\n' | sed '/^$/d' | jq -R . | jq -s .) + + CRITICAL=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "CRITICAL")] | length' \ + checkov-results.json) + HIGH=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "HIGH")] | length' \ + checkov-results.json) + MEDIUM=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "MEDIUM")] | length' \ + checkov-results.json) + LOW=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "LOW")] | length' \ + checkov-results.json) + UNSET=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == null or .severity == "UNKNOWN" or .severity == "NONE")] | length' \ + checkov-results.json) + TOTAL_FAILED=$(( CRITICAL + HIGH + MEDIUM + LOW + UNSET )) + + if [ "$CRITICAL" -gt 0 ]; then + REVIEW_STATE="REQUEST_CHANGES" + VERDICT="BLOCKED: ${CRITICAL} critical finding(s) detected" + EXIT_CODE=1 + elif [ "$HIGH" -gt 0 ]; then + REVIEW_STATE="COMMENT" + VERDICT="WARNING: ${HIGH} high severity finding(s) detected" + EXIT_CODE=0 + else + REVIEW_STATE="APPROVED" + VERDICT="PASSED: No critical or high severity findings" + EXIT_CODE=0 + fi + + DETAILS=$(jq -r --argjson files "$CHANGED_FILES_JSON" ' + [.[] | .check_type as $ct | + .results.failed_checks[]? | + select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | + "| \(.severity // "UNSET") | \(.check_id) | \(.check_name) | \(.file_path) |" + ] | join("\n")' checkov-results.json) + + cat > review-body.md << EOF + ## Checkov IaC Scan Results + + **${VERDICT}** + + > Scanned ${CHANGED_FILES:-"no files"} + + ### Summary + | Severity | Count | + |----------|-------| + | Critical | ${CRITICAL} | + | High | ${HIGH} | + | Medium | ${MEDIUM} | + | Low | ${LOW} | + | Unset | ${UNSET} | + | **Total** | **${TOTAL_FAILED}** | + +
+ Failed Checks (click to expand) + + | Severity | Check ID | Description | File | + |----------|----------|-------------|------| + ${DETAILS} + +
+ + --- + *Scanned by [Checkov](https://github.com/bridgecrewio/checkov)* + EOF + + jq -n \ + --rawfile body review-body.md \ + --arg event "$REVIEW_STATE" \ + '{body: $body, event: $event}' > review-payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @review-payload.json \ + "${GITEA_API}" || echo "Failed to post review (token may not be configured)" + + exit $EXIT_CODE diff --git a/.gitea/workflows/validate.yaml b/.gitea/workflows/validate.yaml new file mode 100644 index 0000000..69541e7 --- /dev/null +++ b/.gitea/workflows/validate.yaml @@ -0,0 +1,95 @@ +name: Validate Manifests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + yaml-lint: + name: YAML Lint + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install yamllint + run: | + python3 -m pip install --break-system-packages yamllint + + - name: Run yamllint + run: | + yamllint -c .yamllint.yaml . + + kustomize-build: + name: Kustomize Build Test + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl with kustomize + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Test root kustomization + run: | + if [ -f "kustomization.yaml" ]; then + echo "Building root kustomization..." + kubectl kustomize . > /tmp/manifests.yaml + echo "✓ Root kustomization builds successfully" + else + echo "No root kustomization.yaml found" + fi + + - name: Test individual app kustomizations + run: | + find . -maxdepth 2 -name "kustomization.yaml" -not -path "./kustomization.yaml" | while read config; do + app_dir=$(dirname "$config") + echo "Building $app_dir kustomization..." + kubectl kustomize "$app_dir" > /dev/null + echo "✓ $app_dir kustomization builds successfully" + done + + kubeconform: + name: Kubernetes Schema Validation + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and kubeconform + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install kubeconform + curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xz + chmod +x kubeconform + mv kubeconform /usr/local/bin/ + + - name: Validate Kubernetes manifests + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . | kubeconform \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \ + -summary \ + -output text \ + -ignore-missing-schemas \ + -skip HTTPRoute \ + -verbose + fi diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..95b2311 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,5 @@ +# Avahi daemon in base images often triggers this +CVE-2021-26720 + +# Common false positives or accepted risks +CVE-2023-52425 diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..33b82f9 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,6 @@ +extends: default + +rules: + line-length: disable + document-start: disable + truthy: disable diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8b93889 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,154 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## ⚠️ CRITICAL: Flux CD Deployment + +**This repository is deployed via Flux CD.** All manifests use Flux variable substitution syntax (e.g., `${VARIABLE_NAME}`). + +**DO NOT replace Flux variables with hardcoded values.** Flux substitutes these variables at deployment time from ConfigMaps or Secrets. + +## Project Overview + +This repository contains Kubernetes manifests for deploying IRC-related applications (The Lounge web client and ZNC bouncer) using Kustomize. The infrastructure is deployed to a Kubernetes cluster with Flux CD and uses Gitea Actions for CI/CD validation and security scanning. + +## Architecture + +### Kustomize Structure +- **Root kustomization.yaml**: Aggregates all application components (thelounge, znc) +- **Application directories**: Each contains its own kustomization.yaml with associated manifests: + - `thelounge/`: Web-based IRC client (StatefulSet, Service, HTTPRoute, NetworkPolicy) + - `znc/`: IRC bouncer (StatefulSet, Service, NetworkPolicy) +- **Commented applications**: bitlbee and inspircd are mentioned but not currently deployed + +### Application Components +Both applications follow the same pattern: +- **StatefulSet**: Deploys the main container with persistent storage via volumeClaimTemplates (4Gi) +- **Service**: Exposes the application (thelounge: 9000, znc: 6501) +- **NetworkPolicy**: Controls network ingress/egress +- **HTTPRoute**: (thelounge only) Gateway API routing configuration + +### Resource Configuration +- Priority class: `low-priority` for both applications +- Resource requests/limits: 100m/500m CPU, 256Mi/512Mi memory +- Security: `automountServiceAccountToken: false`, `allowPrivilegeEscalation: false` +- Probes: Both liveness and readiness probes configured for reliability + +### Polaris Exemptions +All manifests have Polaris exemptions: +- `runAsRootAllowed-exempt`: Containers need root for their base images +- `tagNotSpecified-exempt`: Using `latest` tags +- `topologySpreadConstraint-exempt`: Single-replica deployments don't need spread constraints + +## CI/CD Pipeline + +### Gitea Actions Workflows +Located in `.gitea/workflows/`, three main workflows run on push/PR to main: + +1. **validate.yaml** - Manifest validation: + - YAML linting with yamllint + - Kustomize build tests (root + individual apps) + - Kubernetes schema validation with kubeconform + - Flux build validation + +2. **security.yaml** - Security scanning with PR review automation: + - **Trivy**: Scans for vulnerabilities, posts PR reviews + - **Checkov**: IaC security scanning, posts PR reviews + - PR review states: `REQUEST_CHANGES` (critical), `COMMENT` (high), `APPROVED` (clean) + - Only scans changed YAML/YML/TF files in PRs + +3. **best-practices.yaml** - Kubernetes best practices: + - **kube-score**: Best practices analysis + - **Polaris**: Security and reliability audit with PR reviews + - Resource usage analysis + - Polaris enforces minimum 70% score and blocks on dangers + +### PR Review System +Security and best practices workflows automatically review PRs: +- **Trivy/Checkov**: Critical findings block, high findings warn +- **Polaris**: Danger findings block, warnings comment +- Reviews posted via Gitea API with detailed tables +- Requires tokens: `TRIVY_GITEA_TOKEN`, `CHECKOV_GITEA_TOKEN`, `POLARIS_GITEA_TOKEN` + +## Development Commands + +### Local Validation +```bash +# YAML linting +yamllint -c .yamllint.yaml . + +# Build and validate root kustomization +kubectl kustomize . > /tmp/manifests.yaml + +# Build individual app kustomizations +kubectl kustomize ./thelounge +kubectl kustomize ./znc + +# Validate with kubeconform +kubectl kustomize . | kubeconform \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \ + -summary -ignore-missing-schemas + +# Flux validation +flux build kustomization irc --path . --dry-run +``` + +### Security Scanning +```bash +# Run Trivy config scan +trivy config --severity CRITICAL,HIGH --ignorefile .trivyignore . + +# Run Checkov scan +checkov -d . --config-file .checkov.yaml --output cli +``` + +### Best Practices Analysis +```bash +# Run kube-score +kubectl kustomize . | kube-score score - \ + --ignore-test pod-networkpolicy \ + --ignore-test deployment-has-poddisruptionbudget \ + --ignore-test container-security-context-user-group-id \ + --ignore-test container-security-context-readonlyrootfilesystem + +# Run Polaris audit +kubectl kustomize . | polaris audit --format pretty --set-exit-code-on-danger --set-exit-code-below-score 70 +``` + +## Configuration Files + +### Security and Validation +- `.yamllint.yaml`: YAML linting rules (line-length, document-start, truthy disabled) +- `.checkov.yaml`: Checkov configuration, skips CKV_K8S_21 (namespace) and CKV_K8S_43 (image tags) +- `.trivyignore`: Ignores CVE-2021-26720 (Avahi) and CVE-2023-52425 (accepted risks) +- `configmap.yaml.example`: Template for hostname configuration (not tracked in repo) + +## Key Patterns + +### Adding New Applications +1. Create app directory with kustomization.yaml +2. Add required manifests (statefulset, service, networkpolicy) +3. Reference in root kustomization.yaml resources +4. Include Polaris exemptions if needed +5. Set priorityClassName: low-priority +6. Disable automountServiceAccountToken +7. Configure resource requests/limits and probes + +### Modifying Workflows +- All workflows use `catthehacker/ubuntu:act-latest` container for act compatibility +- PR review jobs require fetch-depth: 0 for git diff operations +- Security tokens should use dedicated secrets (not shared GITEA_TOKEN) +- Exit code 1 on critical findings, 0 on warnings/pass + +## Commit Message Format +When making commits, include credits: +``` +
+ +Generated with [Claude Code](https://claude.ai/code) +via [Happy](https://happy.engineering) + +Co-Authored-By: Claude +Co-Authored-By: Happy +``` diff --git a/README.md b/README.md index 2f02438..3e86239 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,91 @@ -# irc +# IRC Applications -Some useful apps for IRC \ No newline at end of file +Kubernetes manifests for IRC applications, deployed via Flux CD. + +## Applications + +- **The Lounge** - Modern web IRC client with persistent connections +- **ZNC** - IRC bouncer for persistent IRC presence + +## Deployment + +This repository is deployed to Kubernetes using **Flux CD** with variable substitution. Configuration variables (e.g., hostnames) are provided via ConfigMaps at deployment time. + +**Important:** Manifests use Flux variable syntax (`${VARIABLE_NAME}`). Do not replace these with hardcoded values. + +## Architecture + +- **Kustomize-based**: Uses Kustomize for manifest organization +- **StatefulSets**: Both apps use StatefulSets with persistent volumes (4Gi each) +- **Security hardened**: + - Run as non-root (UID 1000) + - Seccomp profiles enabled (RuntimeDefault) + - All capabilities dropped + - Network policies configured +- **Resource managed**: CPU and memory limits set, including ephemeral storage +- **Health checks**: Liveness and readiness probes configured + +## Local Development + +### Validate manifests +```bash +# YAML linting +yamllint -c .yamllint.yaml . + +# Test kustomize builds +kubectl kustomize . +kubectl kustomize ./thelounge +kubectl kustomize ./znc + +# Validate schemas +kubectl kustomize . | kubeconform \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \ + -skip HTTPRoute \ + -ignore-missing-schemas +``` + +### Security scanning +```bash +# Trivy +trivy config --severity CRITICAL,HIGH --ignorefile .trivyignore . + +# Checkov +checkov -d . --config-file .checkov.yaml +``` + +### Best practices +```bash +# Kube-score +kubectl kustomize . | kube-score score - \ + --ignore-test container-image-tag \ + --ignore-test container-security-context-readonlyrootfilesystem + +# Polaris +kubectl kustomize . | polaris audit --format pretty +``` + +## CI/CD + +Automated validation and security scanning via Gitea Actions: + +### Validate Manifests +- YAML linting (yamllint) +- Kustomize build tests +- Kubernetes schema validation (kubeconform, skips HTTPRoute with variables) + +### Security Scan +- **Trivy**: Vulnerability scanning with automated PR reviews +- **Checkov**: IaC security scanning with automated PR reviews +- Blocks PRs on critical findings, warns on high severity + +### Best Practices +- **kube-score**: Kubernetes best practices analysis +- **Polaris**: Security and reliability audit with automated PR reviews +- **Resource analysis**: CPU/memory configuration review + +All workflows run on push/PR to main branch. + +## Documentation + +See [CLAUDE.md](CLAUDE.md) for comprehensive development documentation. \ No newline at end of file diff --git a/thelounge/statefulset.yaml b/thelounge/statefulset.yaml index 5046562..6e5621c 100644 --- a/thelounge/statefulset.yaml +++ b/thelounge/statefulset.yaml @@ -6,11 +6,9 @@ metadata: app.kubernetes.io/name: thelounge app.kubernetes.io/instance: thelounge annotations: - polaris.fairwinds.com/runAsRootAllowed-exempt: "true" polaris.fairwinds.com/tagNotSpecified-exempt: "true" polaris.fairwinds.com/topologySpreadConstraint-exempt: "true" spec: - serviceName: thelounge replicas: 1 selector: matchLabels: @@ -24,11 +22,26 @@ spec: spec: priorityClassName: low-priority automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault containers: - name: thelounge image: ghcr.io/thelounge/thelounge:latest securityContext: allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault ports: - containerPort: 9000 name: http-9000 @@ -39,11 +52,14 @@ spec: requests: cpu: "100m" memory: "256Mi" + ephemeral-storage: "1Gi" limits: cpu: "500m" memory: "512Mi" + ephemeral-storage: "2Gi" livenessProbe: - tcpSocket: + httpGet: + path: / port: 9000 initialDelaySeconds: 20 periodSeconds: 10 diff --git a/znc/statefulset.yaml b/znc/statefulset.yaml index c67df76..6311f84 100644 --- a/znc/statefulset.yaml +++ b/znc/statefulset.yaml @@ -6,7 +6,6 @@ metadata: app.kubernetes.io/name: znc app.kubernetes.io/instance: znc annotations: - polaris.fairwinds.com/runAsRootAllowed-exempt: "true" polaris.fairwinds.com/tagNotSpecified-exempt: "true" polaris.fairwinds.com/topologySpreadConstraint-exempt: "true" spec: @@ -14,7 +13,6 @@ spec: matchLabels: app.kubernetes.io/name: znc app.kubernetes.io/instance: znc - serviceName: "znc" replicas: 1 template: metadata: @@ -24,6 +22,13 @@ spec: spec: priorityClassName: low-priority automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault containers: - name: znc image: lscr.io/linuxserver/znc:latest @@ -33,9 +38,15 @@ spec: name: irc-6501 securityContext: - runAsNonRoot: false allowPrivilegeEscalation: false - privileged: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault volumeMounts: - name: config @@ -45,9 +56,11 @@ spec: requests: memory: "256Mi" cpu: "100m" + ephemeral-storage: "1Gi" limits: memory: "512Mi" cpu: "500m" + ephemeral-storage: "2Gi" livenessProbe: tcpSocket: @@ -73,4 +86,4 @@ spec: - ReadWriteOnce resources: requests: - storage: 4Gi \ No newline at end of file + storage: 4Gi