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