diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..f2f03e8 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,26 @@ +# Detect committed secrets (tokens, keys, passwords) on main and PRs targeting main. +name: Secret scan + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..efeeb34 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,15 @@ +# gitleaks config — https://github.com/gitleaks/gitleaks +title = "ReaperC2 gitleaks" + +[extend] +useDefault = true + +# Template/example credentials are placeholders, not live secrets. +[[allowlists]] +description = "k3s deployment credential templates" +paths = [ + '''deployments/k3s/mongo-secret\.yaml''', + '''deployments/k3s/admin-bootstrap-secret\.yaml''', + '''deployments/k3s/config\.example\.env''', + '''\.env\.example''', +] diff --git a/deployments/README.md b/deployments/README.md index 78d68c2..21389fe 100644 --- a/deployments/README.md +++ b/deployments/README.md @@ -29,11 +29,17 @@ REGISTRY_PASSWORD='your-password-here' | Variable | Purpose | |----------|---------| -| `MONGO_NFS_SERVER` / `MONGO_NFS_PATH` | NFS export for MongoDB PV | +| `MONGO_NFS_SERVER` / `MONGO_NFS_PATH` | NFS export for MongoDB PV (e.g. `192.168.1.100:/export/reaperc2-metric`) | +| `MONGO_DATABASE` | Mongo database name (default `reaperc2-metric`) | +| `MONGO_NODE_HOSTNAME` | Node for Mongo StatefulSet (e.g. `control-plane`; mongo:7 may need ARMv8.2+) | | `REGISTRY_SERVER` / `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | Private registry push/pull | | `REGISTRY_SECRET_NAME` | Kubernetes pull secret name (default `registry-credentials`) | | `REAPERC2_IMAGE` / `REAPERC2_IMAGE_TAG` | Container image to deploy | -| `OLLAMA_PULL_MODELS` | Comma-separated tags pulled on first Ollama start (e.g. `llama3.2:latest,gpt-oss:latest`) | +| `INGRESS_HOST` | Public beacon hostname (default `beacons.example.com`) | +| `CERT_MANAGER_ISSUER` | ClusterIssuer for beacon TLS (default `letsencrypt-prod`) | +| `OLLAMA_NFS_SERVER` / `OLLAMA_NFS_PATH` | NFS export for Ollama model weights (e.g. `/export/ollama-models`) | +| `OLLAMA_PULL_MODELS` | Comma-separated tags pulled on first Ollama start | +| `OLLAMA_STORAGE_CLASS` | Fallback PVC class only if `OLLAMA_NFS_*` is unset | | `REAPERC2_NAMESPACE` | Default `reaperc2-ns` | | `KUBECONFIG` | Optional kubeconfig path | @@ -43,7 +49,9 @@ REGISTRY_PASSWORD='your-password-here' ## Mongo secrets (OnPrem `k3s/`) -Edit [`k3s/mongo-secret.yaml`](k3s/mongo-secret.yaml) before first deploy (`root_*`, `app_*`). Do not change passwords after Mongo has initialized the NFS volume without resetting data. +Edit [`k3s/mongo-secret.yaml`](k3s/mongo-secret.yaml) before first deploy (`root_*`, `app_*`). Database name is **`reaperc2-metric`** (`MONGO_DATABASE`). Do not change passwords after Mongo has initialized the NFS volume without resetting data. + +**Ingress:** beacons only on `INGRESS_HOST` (default `beacons.example.com`). Admin UI uses `kubectl port-forward … 8443:8443` — not exposed on ingress (same as [`k8s/reaperc2/`](k8s/reaperc2/)). --- diff --git a/deployments/k3s/README.md b/deployments/k3s/README.md index d3de562..4400c6b 100644 --- a/deployments/k3s/README.md +++ b/deployments/k3s/README.md @@ -4,48 +4,71 @@ Deploy ReaperC2 on **k3s** with **NFS MongoDB**, a **private registry** image, a > **Not** the DocumentDB path. For k3s + **Amazon DocumentDB**, use [`../k8s/reaperc2/`](../k8s/reaperc2/) with `REAPER_CLUSTER=k3s` ([`../k8s/k3s/README.md`](../k8s/k3s/README.md)). +## Hostnames + +| Surface | Access | +|---------|--------| +| **Beacons** | Public HTTPS on `INGRESS_HOST` (default `beacons.example.com`) → service `:8080` (`deploy.sh --with-ingress`) | +| **Admin UI** | **Port-forward only** (same as AWS reaperc2 path — not on public ingress) | +| **Operator AI** | `/ai` after admin port-forward | + ## Prerequisites - k3s (`curl -sfL https://get.k3s.io | sh -`) - `kubectl` or `sudo k3s kubectl` -- NFS export for MongoDB; `nfs-common` on nodes +- NFS export for MongoDB (e.g. `/export/reaperc2-metric` on your NAS); `nfs-common` on nodes +- Pin MongoDB to a capable node if needed (`MONGO_NODE_HOSTNAME=control-plane`; mongo:7 needs ARMv8.2 on some ARM boards) - Go on the build machine (for `build-push-image.sh`) - ≥ 4 GiB RAM recommended for Ollama +- For public beacons: Traefik + cert-manager with ClusterIssuer `letsencrypt-prod` (or set `CERT_MANAGER_ISSUER` in `config.env`) +- DNS for `INGRESS_HOST` → cluster ingress ## Quick start ```bash cp deployments/k3s/config.example.env deployments/k3s/config.env -# Edit config.env, mongo-secret.yaml, k8s/operator-ai.yaml (or operator-ai.local.yaml) +# Edit config.env, mongo-secret.yaml, admin-bootstrap-secret.yaml, k8s/operator-ai.yaml (or operator-ai.local.yaml) ./deployments/k3s/scripts/build-push-image.sh # arm64 default (Pi) -./deployments/k3s/scripts/deploy.sh +./deployments/k3s/scripts/deploy.sh --with-ingress +# Admin UI (never on ingress): kubectl port-forward -n reaperc2-ns deployment/reaperc2-deployment 8443:8443 +# https://127.0.0.1:8443/login — default first operator: admin / changeme (admin-bootstrap-secret.yaml) # http://127.0.0.1:8443/ai ``` +## MongoDB + +- Database name: **`reaperc2-metric`** (`MONGO_DATABASE` in `config.env`) +- NFS path example: `/export/reaperc2-metric` (edit `MONGO_NFS_*` in `config.env`) +- First install requires an **empty** NFS export; app user is bootstrapped on first pod start +- Do not change `mongo-secret.yaml` passwords after Mongo has initialized without resetting NFS data + ## Scripts | Script | Purpose | |--------|---------| | `scripts/build-push-image.sh` | Host cross-compile + push (`--arch arm64\|amd64`, `--import-local`, `--load`) | -| `scripts/deploy.sh` | Ollama + Operator AI + Mongo + ReaperC2 | -| `scripts/seed-mongo.sh` | Seed `api_db` via port-forward | -| `scripts/reroll.sh` | Rebuild image + rollout (`--with-operator-ai`, `--fresh-mongo`) | +| `scripts/deploy.sh` | Ollama + Operator AI + Mongo + ReaperC2; `--with-ingress` for beacon TLS | +| `scripts/seed-mongo.sh` | Seed `reaperc2-metric` via port-forward | +| `scripts/reroll.sh` | Rebuild image + rollout (`--with-operator-ai`, `--fresh-mongo`, `--with-ingress`) | | `scripts/undeploy.sh` | Tear down (`--delete-pvc`) | +| `scripts/verify-mongo-app-user.sh` | Check app user auth against `reaperc2-metric` | ```bash ./deployments/k3s/scripts/deploy.sh --operator-ai-only ./deployments/k3s/scripts/reroll.sh --with-operator-ai +./deployments/k3s/scripts/reroll.sh --fresh-mongo --with-ingress ``` ## Shared manifests | File | Purpose | |------|---------| -| [`../k8s/ollama.yaml`](../k8s/ollama.yaml) | In-cluster Ollama Deployment | +| [`../k8s/ollama.yaml`](../k8s/ollama.yaml) | In-cluster Ollama Deployment (models on NFS via `OLLAMA_NFS_*` in `config.env`) | | [`../k8s/operator-ai.yaml`](../k8s/operator-ai.yaml) | Operator AI ConfigMap + Secret | +| `ingress.yaml` / `ingressroute.yaml` | Beacon `:8080` only (matches AWS reaperc2 ingress pattern) | ## Config reference @@ -55,5 +78,5 @@ See [`../README.md`](../README.md) for `config.env` variables. ```bash ./deployments/k3s/scripts/undeploy.sh -./deployments/k3s/scripts/undeploy.sh --delete-pvc # destructive +./deployments/k3s/scripts/undeploy.sh --delete-pvc # destructive — wipe NFS export before redeploy ``` diff --git a/deployments/k3s/admin-bootstrap-secret.yaml b/deployments/k3s/admin-bootstrap-secret.yaml new file mode 100644 index 0000000..581479a --- /dev/null +++ b/deployments/k3s/admin-bootstrap-secret.yaml @@ -0,0 +1,13 @@ +# First admin UI operator when the MongoDB operators collection is empty. +# Edit before first deploy (or before first ReaperC2 pod start). After bootstrap, +# change password in the UI or insert operators in MongoDB; updating this secret +# alone does not reset existing operators. +apiVersion: v1 +kind: Secret +metadata: + name: reaperc2-admin-bootstrap + namespace: reaperc2-ns +type: Opaque +stringData: + username: admin + password: changeme diff --git a/deployments/k3s/config.example.env b/deployments/k3s/config.example.env index cc4e238..59e03e3 100644 --- a/deployments/k3s/config.example.env +++ b/deployments/k3s/config.example.env @@ -2,7 +2,10 @@ # MongoDB NFS (static PV in mongodb-nfs-pv.yaml) MONGO_NFS_SERVER=192.168.1.100 -MONGO_NFS_PATH=/volume1/reaperc2db +MONGO_NFS_PATH=/export/reaperc2-metric +MONGO_DATABASE=reaperc2-metric +# mongo:7 needs Pi 5 (ARMv8.2) — must run on control plane, not Pi 4 workers +MONGO_NODE_HOSTNAME=control-plane # Private registry (build-push-image.sh, create-registry-secret.sh) REGISTRY_SERVER=registry.example.com @@ -12,9 +15,23 @@ REGISTRY_SECRET_NAME=registry-credentials REAPERC2_IMAGE=registry.example.com/reaperc2 REAPERC2_IMAGE_TAG=latest +# Pi k3s cluster requires linux/arm64 (default in build-push-image.sh) +REAPERC2_BUILD_ARCH=arm64 -# Ollama model tags pulled on first start (ConfigMap ollama-pull-models) -OLLAMA_PULL_MODELS=llama3.2:latest,gpt-oss:latest,deepseek-r1:latest +# Public beacon hostname (admin UI is port-forward only — same as AWS reaperc2 path) +INGRESS_HOST=beacons.example.com +CERT_MANAGER_ISSUER=letsencrypt-prod + +# Ollama model storage on NFS (/root/.ollama in the pod → this export). +OLLAMA_NFS_SERVER=192.168.1.100 +OLLAMA_NFS_PATH=/export/ollama-models +# Ollama model tags pulled on first start (ConfigMap ollama-pull-models). +# :cloud tags (e.g. glm-5.2:cloud) proxy to Ollama Cloud — require OLLAMA_API_KEY below. +OLLAMA_PULL_MODELS=llama3.2:latest,gpt-oss:latest,deepseek-r1:latest,glm-5.2:cloud +# Fallback only when OLLAMA_NFS_* is unset (not recommended on k3s) +# OLLAMA_STORAGE_CLASS=local-path +# API key from https://ollama.com/settings/keys — required for :cloud models in-cluster +# OLLAMA_API_KEY= # Operator AI: set cloud keys here or patch deployments/k8s/operator-ai.yaml Secret # REAPER_AI_OPENAI_API_KEY= diff --git a/deployments/k3s/deployment.yaml b/deployments/k3s/deployment.yaml index c97fb38..fc94d36 100644 --- a/deployments/k3s/deployment.yaml +++ b/deployments/k3s/deployment.yaml @@ -45,9 +45,19 @@ spec: name: reaperc2-mongodb-secrets key: app_password - name: MONGO_DATABASE - value: "api_db" + value: "reaperc2-metric" - name: REAPERC2_ROOT value: "/root" + - name: ADMIN_BOOTSTRAP_USERNAME + valueFrom: + secretKeyRef: + name: reaperc2-admin-bootstrap + key: username + - name: ADMIN_BOOTSTRAP_PASSWORD + valueFrom: + secretKeyRef: + name: reaperc2-admin-bootstrap + key: password envFrom: - configMapRef: name: reaperc2-ai-config diff --git a/deployments/k3s/ingress-http-redirect.yaml b/deployments/k3s/ingress-http-redirect.yaml new file mode 100644 index 0000000..23e6620 --- /dev/null +++ b/deployments/k3s/ingress-http-redirect.yaml @@ -0,0 +1,22 @@ +# Port 80 → 301 redirect to HTTPS. cert-manager HTTP-01 uses separate challenge routes. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: reaperc2-http-redirect + namespace: reaperc2-ns + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: reaperc2-ns-redirect-https@kubernetescrd +spec: + ingressClassName: traefik + rules: + - host: beacons.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: reaperc2-service + port: + number: 8080 diff --git a/deployments/k3s/ingress.yaml b/deployments/k3s/ingress.yaml new file mode 100644 index 0000000..0bff76c --- /dev/null +++ b/deployments/k3s/ingress.yaml @@ -0,0 +1,30 @@ +# Beacon traffic only (port 8080). Admin UI is not on public ingress — use port-forward :8443. +# Apply after core stack: ./deployments/k3s/scripts/deploy.sh --with-ingress +# Requires Traefik IngressClass + cert-manager ClusterIssuer (CERT_MANAGER_ISSUER in config.env). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: reaperc2-ingress + namespace: reaperc2-ns + annotations: + acme.cert-manager.io/http01-edit-in-place: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: reaperc2-ns-security-headers@kubernetescrd,reaperc2-ns-per-ip-ratelimit@kubernetescrd +spec: + ingressClassName: traefik + rules: + - host: beacons.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: reaperc2-service + port: + number: 8080 + tls: + - hosts: + - beacons.example.com + secretName: beacons-example-com-tls diff --git a/deployments/k3s/ingressroute.yaml b/deployments/k3s/ingressroute.yaml new file mode 100644 index 0000000..7b459b9 --- /dev/null +++ b/deployments/k3s/ingressroute.yaml @@ -0,0 +1,20 @@ +# Traefik IngressRoute for beacon HTTPS (matches TLS secret from ingress.yaml). +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: reaperc2-ingressroute + namespace: reaperc2-ns +spec: + entryPoints: + - websecure + routes: + - match: Host(`beacons.example.com`) + kind: Rule + middlewares: + - name: security-headers + - name: per-ip-ratelimit + services: + - name: reaperc2-service + port: 8080 + tls: + secretName: beacons-example-com-tls diff --git a/deployments/k3s/mongo-secret.yaml b/deployments/k3s/mongo-secret.yaml index 10e27cf..16b96e8 100644 --- a/deployments/k3s/mongo-secret.yaml +++ b/deployments/k3s/mongo-secret.yaml @@ -1,4 +1,5 @@ # MongoDB credentials for in-cluster mongo (k3s/mongodb.yaml). +# App user is created on database reaperc2-metric at first boot (mongo/init-app-user.js). # Edit before first deploy. After Mongo has initialized the volume, changing root_* # or app_* requires resetting the NFS export (see k3s/README.md). apiVersion: v1 diff --git a/deployments/k3s/mongo/init-app-user.js b/deployments/k3s/mongo/init-app-user.js new file mode 100644 index 0000000..6199e46 --- /dev/null +++ b/deployments/k3s/mongo/init-app-user.js @@ -0,0 +1,24 @@ +// Creates readWrite user on reaperc2-metric DB (runs once on empty data dir). +// Requires env: MONGO_APP_USERNAME, MONGO_APP_PASSWORD +const user = process.env.MONGO_APP_USERNAME; +const pwd = process.env.MONGO_APP_PASSWORD; +if (!user || !pwd) { + throw new Error("MONGO_APP_USERNAME and MONGO_APP_PASSWORD must be set"); +} + +const dbName = "reaperc2-metric"; +db = db.getSiblingDB(dbName); + +if (db.getUser(user)) { + db.updateUser(user, { pwd }); + print(`Updated password for app user ${user} on ${dbName}`); + quit(0); +} + +db.createUser({ + user, + pwd, + roles: [{ role: "readWrite", db: dbName }], +}); + +print(`Created app user ${user} on ${dbName}`); diff --git a/deployments/k3s/mongo/init-app-user.sh b/deployments/k3s/mongo/init-app-user.sh new file mode 100755 index 0000000..7bf1a75 --- /dev/null +++ b/deployments/k3s/mongo/init-app-user.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Runs after root user is created (mongo:7 entrypoint, first init only). +set -euo pipefail + +mongosh -u "${MONGO_INITDB_ROOT_USERNAME}" -p "${MONGO_INITDB_ROOT_PASSWORD}" \ + --authenticationDatabase admin \ + /docker-entrypoint-initdb.d/init-app-user.js diff --git a/deployments/k3s/mongo/mongo-healthcheck.sh b/deployments/k3s/mongo/mongo-healthcheck.sh new file mode 100755 index 0000000..1d2c03d --- /dev/null +++ b/deployments/k3s/mongo/mongo-healthcheck.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Probe script — password passed to mongosh as argv (safe for special characters). +set -e +mongosh --quiet \ + -u "${MONGO_INITDB_ROOT_USERNAME}" \ + -p "${MONGO_INITDB_ROOT_PASSWORD}" \ + --authenticationDatabase admin \ + --eval 'db.adminCommand("ping").ok' diff --git a/deployments/k3s/mongodb-nfs-pv.yaml b/deployments/k3s/mongodb-nfs-pv.yaml index e03acd5..9f42f36 100644 --- a/deployments/k3s/mongodb-nfs-pv.yaml +++ b/deployments/k3s/mongodb-nfs-pv.yaml @@ -15,4 +15,4 @@ spec: storageClassName: "" nfs: server: 192.168.1.100 - path: /volume1/reaperc2db + path: /export/reaperc2-metric diff --git a/deployments/k3s/mongodb.yaml b/deployments/k3s/mongodb.yaml index 56a10ad..03b23ba 100644 --- a/deployments/k3s/mongodb.yaml +++ b/deployments/k3s/mongodb.yaml @@ -1,5 +1,8 @@ -# MongoDB on NFS (see mongodb-nfs-pv.yaml). Seed api_db after first boot: -# ./deployments/k3s/scripts/seed-mongo.sh +# MongoDB on NFS (see mongodb-nfs-pv.yaml). App user bootstrapped on first empty volume; +# seed collections after deploy: ./deployments/k3s/scripts/seed-mongo.sh +# +# mongo:7 requires ARMv8.2-A (Pi 5 / BCM2712). Pi 4 workers fail with illegal instruction. +# Pin to the control-plane node via nodeSelector (MONGO_NODE_HOSTNAME in config.env). apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -30,6 +33,8 @@ spec: labels: app: mongo spec: + nodeSelector: + kubernetes.io/hostname: control-plane containers: - name: mongo image: mongo:7 @@ -46,25 +51,35 @@ spec: secretKeyRef: name: reaperc2-mongodb-secrets key: root_password + - name: MONGO_APP_USERNAME + valueFrom: + secretKeyRef: + name: reaperc2-mongodb-secrets + key: app_username + - name: MONGO_APP_PASSWORD + valueFrom: + secretKeyRef: + name: reaperc2-mongodb-secrets + key: app_password volumeMounts: - name: data mountPath: /data/db + - name: mongo-scripts + mountPath: /scripts + readOnly: true + - name: mongo-init + mountPath: /docker-entrypoint-initdb.d + readOnly: true readinessProbe: exec: command: - - mongosh - - --quiet - - --eval - - "db.adminCommand('ping')" + - /scripts/mongo-healthcheck.sh initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: exec: command: - - mongosh - - --quiet - - --eval - - "db.adminCommand('ping')" + - /scripts/mongo-healthcheck.sh initialDelaySeconds: 30 periodSeconds: 10 resources: @@ -78,6 +93,14 @@ spec: - name: data persistentVolumeClaim: claimName: mongo-pvc + - name: mongo-scripts + configMap: + name: mongo-scripts + defaultMode: 0755 + - name: mongo-init + configMap: + name: mongo-init + defaultMode: 0755 --- apiVersion: v1 kind: Service diff --git a/deployments/k3s/ollama-nfs-pv.yaml b/deployments/k3s/ollama-nfs-pv.yaml new file mode 100644 index 0000000..4e2b977 --- /dev/null +++ b/deployments/k3s/ollama-nfs-pv.yaml @@ -0,0 +1,18 @@ +# Optional static NFS volume for Ollama local model weights (/root/.ollama). +# Enable by setting OLLAMA_NFS_SERVER and OLLAMA_NFS_PATH in k3s/config.env. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: reaperc2-ollama-nfs-pv + labels: + app: ollama +spec: + capacity: + storage: 50Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + nfs: + server: 192.168.1.100 + path: /export/ollama-models diff --git a/deployments/k3s/ollama-pvc-nfs.yaml b/deployments/k3s/ollama-pvc-nfs.yaml new file mode 100644 index 0000000..d853809 --- /dev/null +++ b/deployments/k3s/ollama-pvc-nfs.yaml @@ -0,0 +1,16 @@ +# NFS-backed PVC for Ollama model storage. Used when OLLAMA_NFS_* is set in config.env. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ollama-data + namespace: ollama-ns + labels: + app: ollama +spec: + accessModes: + - ReadWriteOnce + storageClassName: "" + volumeName: reaperc2-ollama-nfs-pv + resources: + requests: + storage: 50Gi diff --git a/deployments/k3s/scripts/build-push-image.sh b/deployments/k3s/scripts/build-push-image.sh index a54c058..0bd9dac 100755 --- a/deployments/k3s/scripts/build-push-image.sh +++ b/deployments/k3s/scripts/build-push-image.sh @@ -7,7 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/lib.sh" load_config -ARCH="${ARCH:-arm64}" +ARCH="${ARCH:-${REAPERC2_BUILD_ARCH:-arm64}}" IMPORT_LOCAL=false PUSH=true diff --git a/deployments/k3s/scripts/common.sh b/deployments/k3s/scripts/common.sh index 3e0ffc0..4940712 100755 --- a/deployments/k3s/scripts/common.sh +++ b/deployments/k3s/scripts/common.sh @@ -84,11 +84,62 @@ patch_mongo_nfs_pv() { echo "PV file not found: $pv_file" >&2 exit 1 fi - sed -e "s|server: 192.168.1.100|server: ${server}|" \ - -e "s|path: /volume1/reaperc2db|path: ${path}|" \ + sed -e "s|server: .*|server: ${server}|" \ + -e "s|path: /export/.*|path: ${path}|" \ "$pv_file" | kubectl_cmd apply -f - } +apply_ollama_stack() { + local k8s_root="$1" + local k3s_root="${2:-}" + local sc="${OLLAMA_STORAGE_CLASS:-local-path}" + echo "Applying Ollama (Operator AI backend)..." + kubectl_cmd create namespace ollama-ns --dry-run=client -o yaml | kubectl_cmd apply -f - + if [[ -n "${OLLAMA_NFS_SERVER:-}" && -n "${OLLAMA_NFS_PATH:-}" && -n "$k3s_root" ]]; then + sed -e "s|server: .*|server: ${OLLAMA_NFS_SERVER}|" \ + -e "s|path: /export/.*|path: ${OLLAMA_NFS_PATH}|" \ + "${k3s_root}/ollama-nfs-pv.yaml" | kubectl_cmd apply -f - + kubectl_cmd apply -f "${k3s_root}/ollama-pvc-nfs.yaml" + sed -e '/^kind: PersistentVolumeClaim$/,/^---$/d' \ + "${k8s_root}/ollama.yaml" | kubectl_cmd apply -f - + elif [[ -n "$sc" ]]; then + sed -e "s|storageClassName: gp2|storageClassName: ${sc}|" \ + "${k8s_root}/ollama.yaml" | kubectl_cmd apply -f - + else + sed -e '/^[[:space:]]*storageClassName: gp2/d' \ + "${k8s_root}/ollama.yaml" | kubectl_cmd apply -f - + fi + apply_ollama_pull_models + apply_ollama_api_key_secret + echo "Waiting for Ollama PVC..." + kubectl_cmd -n ollama-ns wait --for=jsonpath='{.status.phase}'=Bound pvc/ollama-data --timeout=180s || true + kubectl_cmd -n ollama-ns rollout status deploy/ollama --timeout=3600s || true +} + +apply_operator_ai() { + local k8s_root="$1" + local ns="${REAPERC2_NAMESPACE:-reaperc2-ns}" + echo "Applying Operator AI config..." + kubectl_cmd apply -f "${k8s_root}/operator-ai.yaml" + local ai_patch='{"data":{"REAPER_AI_OLLAMA_ENABLED":"0","REAPER_AI_DEFAULT_PROVIDER":"bedrock"}}' + if kubectl_cmd get deploy ollama -n ollama-ns >/dev/null 2>&1; then + ai_patch='{"data":{"REAPER_AI_OLLAMA_ENABLED":"1","REAPER_AI_OLLAMA_DISCOVER":"1","REAPER_AI_DEFAULT_PROVIDER":"ollama"}}' + echo "In-cluster Ollama detected — Operator AI will use Ollama provider." + fi + kubectl_cmd patch configmap reaperc2-ai-config -n "$ns" \ + --type merge -p "$ai_patch" 2>/dev/null || true +} + +apply_ollama_api_key_secret() { + if [[ -z "${OLLAMA_API_KEY:-}" ]]; then + return 0 + fi + kubectl_cmd create secret generic ollama-api-key \ + --from-literal=OLLAMA_API_KEY="${OLLAMA_API_KEY}" \ + -n ollama-ns \ + --dry-run=client -o yaml | kubectl_cmd apply -f - +} + prepare_reaperc2_build() { local repo_root="$1" local arch="$2" @@ -145,7 +196,7 @@ docker_build_push() { local tar tar="$(mktemp /tmp/reaperc2-XXXXXX.tar)" trap 'rm -f "$tar"' RETURN - docker pull "${image}" + docker pull --platform "${platform}" "${image}" docker save "${image}" -o "$tar" if command -v k3s >/dev/null 2>&1; then sudo k3s ctr images import "$tar" @@ -169,24 +220,64 @@ create_registry_pull_secret() { --dry-run=client -o yaml | kubectl_cmd apply -f - } -apply_ollama_stack() { - local k8s_root="$1" - echo "Applying Ollama (Operator AI backend)..." - kubectl_cmd apply -f "${k8s_root}/ollama.yaml" - apply_ollama_pull_models - echo "Waiting for Ollama PVC..." - kubectl_cmd -n ollama-ns wait --for=jsonpath='{.status.phase}'=Bound pvc/ollama-data --timeout=180s || true - kubectl_cmd -n ollama-ns rollout status deploy/ollama --timeout=900s || true -} - -apply_operator_ai() { - local k8s_root="$1" - echo "Applying Operator AI config..." - kubectl_cmd apply -f "${k8s_root}/operator-ai.yaml" -} - rollout_reaperc2() { local namespace="$1" kubectl_cmd -n "$namespace" rollout restart deployment/reaperc2-deployment kubectl_cmd -n "$namespace" rollout status deployment/reaperc2-deployment --timeout=300s } + +patch_k3s_ingress_host() { + local file="$1" + local host="$2" + local issuer="$3" + sed -e "s|beacons\\.example\\.com|${host}|g" \ + -e "s|cert-manager.io/cluster-issuer: .*|cert-manager.io/cluster-issuer: ${issuer}|" \ + "$file" +} + +apply_k3s_ingress() { + local k3s_root="$1" + local host="${INGRESS_HOST:-beacons.example.com}" + local issuer="${CERT_MANAGER_ISSUER:-letsencrypt-prod}" + local ns="${REAPERC2_NAMESPACE:-reaperc2-ns}" + + if ! kubectl_cmd get clusterissuer "$issuer" >/dev/null 2>&1; then + echo "ClusterIssuer ${issuer} not found — install cert-manager and create the issuer first." >&2 + exit 1 + fi + + echo "Applying beacon ingress for ${host} (admin UI remains port-forward only)..." + patch_k3s_ingress_host "${k3s_root}/traefik-redirect-https-middleware.yaml" "$host" "$issuer" \ + | kubectl_cmd apply -f - + patch_k3s_ingress_host "${k3s_root}/traefik-security-middleware.yaml" "$host" "$issuer" \ + | kubectl_cmd apply -f - + patch_k3s_ingress_host "${k3s_root}/ingress-http-redirect.yaml" "$host" "$issuer" \ + | kubectl_cmd apply -f - + patch_k3s_ingress_host "${k3s_root}/ingress.yaml" "$host" "$issuer" \ + | kubectl_cmd apply -f - + patch_k3s_ingress_host "${k3s_root}/ingressroute.yaml" "$host" "$issuer" \ + | kubectl_cmd apply -f - + kubectl_cmd -n "$ns" get ingress,ingressroute 2>/dev/null || true +} + +teardown_k3s_ingress() { + local k3s_root="$1" + local ns="${REAPERC2_NAMESPACE:-reaperc2-ns}" + local tls_secret="beacons-example-com-tls" + local ing_name="reaperc2-ingress" + + echo "Removing ingress, middlewares, and staging/prod TLS resources..." + kubectl_cmd delete -f "${k3s_root}/ingress.yaml" -f "${k3s_root}/ingressroute.yaml" \ + -f "${k3s_root}/ingress-http-redirect.yaml" \ + -f "${k3s_root}/traefik-redirect-https-middleware.yaml" \ + -f "${k3s_root}/traefik-security-middleware.yaml" \ + -n "$ns" --ignore-not-found + + if kubectl_cmd get crd certificates.cert-manager.io >/dev/null 2>&1; then + kubectl_cmd delete certificate -n "$ns" -l "cert-manager.io/ingress-name=${ing_name}" --ignore-not-found + kubectl_cmd delete certificate -n "$ns" "$tls_secret" --ignore-not-found + kubectl_cmd delete certificaterequest -n "$ns" --all --ignore-not-found + fi + kubectl_cmd delete secret "$tls_secret" -n "$ns" --ignore-not-found + kubectl_cmd delete pod -n "$ns" -l acme.cert-manager.io/http01-solver=true --ignore-not-found +} diff --git a/deployments/k3s/scripts/deploy.sh b/deployments/k3s/scripts/deploy.sh index 0a764e9..96ba26d 100755 --- a/deployments/k3s/scripts/deploy.sh +++ b/deployments/k3s/scripts/deploy.sh @@ -6,11 +6,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/lib.sh" load_config -WITH_OLLAMA=true +WITH_OLLAMA=false WITH_OPERATOR_AI=true +WITH_INGRESS=false SKIP_REGISTRY_SECRET=false SKIP_MONGO_SEED=false OPERATOR_AI_ONLY=false +OLLAMA_ONLY=false usage() { cat <<'EOF' @@ -18,37 +20,64 @@ Usage: deployments/k3s/scripts/deploy.sh [options] Deploy ReaperC2 on k3s with optional in-cluster Ollama for Operator AI. +Ollama is **off by default** (`--no-ollama` is the default). Use cloud providers in +operator-ai.yaml instead, or pass explicit flags to enable Ollama if you add it back. + +Slow rollout: deploy Ollama first (model pulls can take 30+ minutes on Pi): + ./deployments/k3s/scripts/deploy.sh --ollama-only + +Beacon traffic: use --with-ingress for public HTTPS on INGRESS_HOST (default +beacons.example.com → service :8080). Admin UI is never on ingress; +use kubectl port-forward :8443 (same as AWS reaperc2 path). + Options: - --no-ollama Skip Ollama Deployment (use external Ollama URL in operator-ai.yaml) + --ollama-only Apply Ollama only (NFS + model pull); skip ReaperC2/Mongo + --with-ollama Apply in-cluster Ollama with full deploy + --with-ingress Apply Traefik ingress + IngressRoute for beacons + --no-ollama Skip Ollama Deployment (default) --no-operator-ai Skip Operator AI ConfigMap/Secret --skip-registry-secret Do not create registry pull secret --skip-mongo-seed Do not run test/setup_mongo.sh after Mongo is ready - --operator-ai-only Apply Ollama + operator-ai.yaml and restart ReaperC2 only + --operator-ai-only Apply operator-ai.yaml and restart ReaperC2 only (add --with-ollama to include Ollama) -h, --help Show help Prerequisites: - k3s with kubectl (or sudo k3s kubectl) - - NFS export matching MONGO_NFS_* in k3s/config.env (or edit mongodb-nfs-pv.yaml) - - k3s/mongo-secret.yaml credentials set + - Empty NFS export matching MONGO_NFS_* in k3s/config.env (first install) + - k3s/mongo-secret.yaml and admin-bootstrap-secret.yaml credentials set - Image built/pushed: ./deployments/k3s/scripts/build-push-image.sh + - For --with-ingress: Traefik + cert-manager ClusterIssuer (CERT_MANAGER_ISSUER) EOF } while [[ $# -gt 0 ]]; do case "$1" in + --with-ingress) WITH_INGRESS=true; shift ;; + --with-ollama) WITH_OLLAMA=true; shift ;; --no-ollama) WITH_OLLAMA=false; shift ;; --no-operator-ai) WITH_OPERATOR_AI=false; shift ;; --skip-registry-secret) SKIP_REGISTRY_SECRET=true; shift ;; --skip-mongo-seed) SKIP_MONGO_SEED=true; shift ;; --operator-ai-only) OPERATOR_AI_ONLY=true; shift ;; + --ollama-only) OLLAMA_ONLY=true; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage; exit 1 ;; esac done +if [[ "$OLLAMA_ONLY" == true ]]; then + echo "Ollama-only deploy (models: ${OLLAMA_PULL_MODELS:-from config.env})..." + echo "Init pull can take a long time on Pi — watch: kubectl logs -n ollama-ns -l app=ollama -c pull-models -f" + apply_ollama_stack "$K8S_ROOT" "$K3S_ROOT" + echo "" + echo "Ollama stack applied." + kubectl_cmd -n ollama-ns get pods,svc,pvc,pv 2>/dev/null || true + exit 0 +fi + if [[ "$OPERATOR_AI_ONLY" == true ]]; then if [[ "$WITH_OLLAMA" == true ]]; then - apply_ollama_stack "$K8S_ROOT" + apply_ollama_stack "$K8S_ROOT" "$K3S_ROOT" fi if [[ "$WITH_OPERATOR_AI" == true ]]; then apply_operator_ai "$K8S_ROOT" @@ -59,6 +88,9 @@ if [[ "$OPERATOR_AI_ONLY" == true ]]; then fi require_file "${K3S_ROOT}/mongo-secret.yaml" "Edit deployments/k3s/mongo-secret.yaml before deploy" +require_file "${K3S_ROOT}/admin-bootstrap-secret.yaml" "Edit deployments/k3s/admin-bootstrap-secret.yaml before deploy" + +python3 "${SCRIPT_DIR}/verify-mongo-secret.py" --mongo-secret "${K3S_ROOT}/mongo-secret.yaml" if [[ "$SKIP_REGISTRY_SECRET" != true ]]; then if [[ -n "${REGISTRY_USERNAME:-}" && -n "${REGISTRY_PASSWORD:-}" ]]; then @@ -72,7 +104,7 @@ echo "Applying namespaces..." kubectl_cmd apply -f "${K3S_ROOT}/namespace.yaml" if [[ "$WITH_OLLAMA" == true ]]; then - apply_ollama_stack "$K8S_ROOT" + apply_ollama_stack "$K8S_ROOT" "$K3S_ROOT" fi if [[ "$WITH_OPERATOR_AI" == true ]]; then @@ -82,31 +114,49 @@ fi echo "Applying MongoDB..." kubectl_cmd apply -f "${K3S_ROOT}/mongo-secret.yaml" apply_k3s_mongo_nfs_pv -kubectl_cmd apply -f "${K3S_ROOT}/mongodb.yaml" +apply_k3s_mongo_configmaps +apply_k3s_mongodb echo "Waiting for MongoDB PVC..." -kubectl_cmd -n reaperc2-ns wait --for=jsonpath='{.status.phase}'=Bound pvc/mongo-pvc --timeout=180s -kubectl_cmd -n reaperc2-ns rollout status statefulset/mongo --timeout=600s +kubectl_cmd -n "$REAPERC2_NAMESPACE" wait --for=jsonpath='{.status.phase}'=Bound pvc/mongo-pvc --timeout=180s +echo "Waiting for MongoDB pod (first empty NFS volume runs init: root + app user)..." +kubectl_cmd -n "$REAPERC2_NAMESPACE" rollout status statefulset/mongo --timeout=600s + +echo "Verifying app user can authenticate..." +if ! "${SCRIPT_DIR}/verify-mongo-app-user.sh"; then + echo "Retrying app user bootstrap on mongo pod..." + "${SCRIPT_DIR}/mongo-bootstrap-app-user.sh" + "${SCRIPT_DIR}/verify-mongo-app-user.sh" +fi if [[ "$SKIP_MONGO_SEED" != true ]]; then - echo "Seeding api_db (test/setup_mongo.sh)..." + echo "Seeding ${MONGO_DATABASE} (test/setup_mongo.sh)..." "${SCRIPT_DIR}/seed-mongo.sh" fi echo "Applying ReaperC2..." +kubectl_cmd apply -f "${K3S_ROOT}/admin-bootstrap-secret.yaml" kubectl_cmd apply -f "${K3S_ROOT}/deployment.yaml" kubectl_cmd apply -f "${K3S_ROOT}/service.yaml" -kubectl_cmd -n reaperc2-ns set image deployment/reaperc2-deployment \ +kubectl_cmd -n "$REAPERC2_NAMESPACE" set image deployment/reaperc2-deployment \ reaperc2="$(reaperc2_image_ref)" -kubectl_cmd -n reaperc2-ns rollout status deployment/reaperc2-deployment --timeout=300s +kubectl_cmd -n "$REAPERC2_NAMESPACE" rollout status deployment/reaperc2-deployment --timeout=300s + +if [[ "$WITH_INGRESS" == true ]]; then + apply_k3s_ingress "$K3S_ROOT" +fi echo "" echo "Deploy complete." -kubectl_cmd -n reaperc2-ns get pods,svc,pvc +kubectl_cmd -n "$REAPERC2_NAMESPACE" get pods,svc,pvc if [[ "$WITH_OLLAMA" == true ]]; then kubectl_cmd -n ollama-ns get pods,svc,pvc fi echo "" -echo "Admin UI: kubectl port-forward -n reaperc2-ns deployment/reaperc2-deployment 8443:8443" +if [[ "$WITH_INGRESS" == true ]]; then + echo "Beacon: https://${INGRESS_HOST}/" +fi +echo "Admin UI (port-forward only): kubectl port-forward -n ${REAPERC2_NAMESPACE} deployment/reaperc2-deployment 8443:8443" +echo " Default login (first boot): admin / changeme — edit admin-bootstrap-secret.yaml before deploy to change." echo "Operator AI: open /ai after port-forward (Ollama provider should appear when Ollama pod is ready)." diff --git a/deployments/k3s/scripts/lib.sh b/deployments/k3s/scripts/lib.sh index 5bb6964..42b58f1 100755 --- a/deployments/k3s/scripts/lib.sh +++ b/deployments/k3s/scripts/lib.sh @@ -16,13 +16,21 @@ load_config() { "${K3S_ROOT}/../config.env" \ || true MONGO_NFS_SERVER="${MONGO_NFS_SERVER:-192.168.1.100}" - MONGO_NFS_PATH="${MONGO_NFS_PATH:-/volume1/reaperc2db}" + MONGO_NFS_PATH="${MONGO_NFS_PATH:-/export/reaperc2-metric}" + MONGO_DATABASE="${MONGO_DATABASE:-reaperc2-metric}" + MONGO_NODE_HOSTNAME="${MONGO_NODE_HOSTNAME:-control-plane}" REGISTRY_SERVER="${REGISTRY_SERVER:-registry.example.com}" REGISTRY_SECRET_NAME="${REGISTRY_SECRET_NAME:-registry-credentials}" REAPERC2_IMAGE="${REAPERC2_IMAGE:-registry.example.com/reaperc2}" REAPERC2_IMAGE_TAG="${REAPERC2_IMAGE_TAG:-latest}" + REAPERC2_BUILD_ARCH="${REAPERC2_BUILD_ARCH:-arm64}" OLLAMA_PULL_MODELS="${OLLAMA_PULL_MODELS:-llama3.2:latest}" + OLLAMA_NFS_SERVER="${OLLAMA_NFS_SERVER:-192.168.1.100}" + OLLAMA_NFS_PATH="${OLLAMA_NFS_PATH:-/export/ollama-models}" + OLLAMA_STORAGE_CLASS="${OLLAMA_STORAGE_CLASS:-}" REAPERC2_NAMESPACE="${REAPERC2_NAMESPACE:-reaperc2-ns}" + INGRESS_HOST="${INGRESS_HOST:-beacons.example.com}" + CERT_MANAGER_ISSUER="${CERT_MANAGER_ISSUER:-letsencrypt-prod}" } reaperc2_image_ref() { @@ -32,3 +40,21 @@ reaperc2_image_ref() { apply_k3s_mongo_nfs_pv() { patch_mongo_nfs_pv "${K3S_ROOT}/mongodb-nfs-pv.yaml" "$MONGO_NFS_SERVER" "$MONGO_NFS_PATH" } + +apply_k3s_mongo_configmaps() { + kubectl_cmd create configmap mongo-scripts \ + --from-file=mongo-healthcheck.sh="${K3S_ROOT}/mongo/mongo-healthcheck.sh" \ + -n "$REAPERC2_NAMESPACE" \ + --dry-run=client -o yaml | kubectl_cmd apply -f - + kubectl_cmd create configmap mongo-init \ + --from-file=init-app-user.sh="${K3S_ROOT}/mongo/init-app-user.sh" \ + --from-file=init-app-user.js="${K3S_ROOT}/mongo/init-app-user.js" \ + -n "$REAPERC2_NAMESPACE" \ + --dry-run=client -o yaml | kubectl_cmd apply -f - +} + +apply_k3s_mongodb() { + local node="${MONGO_NODE_HOSTNAME:-control-plane}" + sed -e "s|kubernetes.io/hostname: .*|kubernetes.io/hostname: ${node}|" \ + "${K3S_ROOT}/mongodb.yaml" | kubectl_cmd apply -f - +} diff --git a/deployments/k3s/scripts/mongo-bootstrap-app-user.sh b/deployments/k3s/scripts/mongo-bootstrap-app-user.sh new file mode 100755 index 0000000..3b23e24 --- /dev/null +++ b/deployments/k3s/scripts/mongo-bootstrap-app-user.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Run first-boot app user script on mongo pod (idempotent). Used by deploy.sh if verify fails. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "${SCRIPT_DIR}/lib.sh" +load_config + +NS="${REAPERC2_NAMESPACE}" + +echo "Bootstrapping app user from reaperc2-mongodb-secrets on pod..." +kubectl_cmd -n "$NS" exec statefulset/mongo -- /docker-entrypoint-initdb.d/init-app-user.sh diff --git a/deployments/k3s/scripts/reroll.sh b/deployments/k3s/scripts/reroll.sh index e4c5710..45612bb 100755 --- a/deployments/k3s/scripts/reroll.sh +++ b/deployments/k3s/scripts/reroll.sh @@ -8,6 +8,8 @@ source "${SCRIPT_DIR}/lib.sh" load_config WITH_OPERATOR_AI=false +WITH_INGRESS=false +REFRESH_INGRESS=false FRESH_MONGO=false BUILD_ARGS=() @@ -19,6 +21,8 @@ Rebuild/push image and roll out ReaperC2 (and optionally Operator AI). Options: --with-operator-ai Apply Ollama + operator-ai.yaml before restart + --with-ingress Pass --with-ingress to deploy on --fresh-mongo + --refresh-ingress Tear down TLS/ingress and re-apply (e.g. staging → prod) --fresh-mongo Undeploy with PVC delete and full redeploy (destructive) -h, --help Show help EOF @@ -27,6 +31,8 @@ EOF while [[ $# -gt 0 ]]; do case "$1" in --with-operator-ai) WITH_OPERATOR_AI=true; shift ;; + --with-ingress) WITH_INGRESS=true; shift ;; + --refresh-ingress) REFRESH_INGRESS=true; shift ;; --fresh-mongo) FRESH_MONGO=true; shift ;; --build-arg) BUILD_ARGS+=("$2") @@ -47,20 +53,31 @@ if [[ "$FRESH_MONGO" == true ]]; then exit 1 fi deploy_args=() + [[ "$WITH_INGRESS" == true ]] && deploy_args+=(--with-ingress) "${SCRIPT_DIR}/deploy.sh" "${deploy_args[@]}" exit 0 fi +if [[ "$REFRESH_INGRESS" == true ]]; then + teardown_k3s_ingress "$K3S_ROOT" + apply_k3s_ingress "$K3S_ROOT" + echo "Ingress refreshed (issuer: ${CERT_MANAGER_ISSUER})." + kubectl_cmd -n "$REAPERC2_NAMESPACE" get ingress,ingressroute,certificate 2>/dev/null || true + exit 0 +fi + "${SCRIPT_DIR}/build-push-image.sh" "${BUILD_ARGS[@]}" kubectl_cmd -n "$REAPERC2_NAMESPACE" set image deployment/reaperc2-deployment \ reaperc2="$(reaperc2_image_ref)" if [[ "$WITH_OPERATOR_AI" == true ]]; then - apply_ollama_stack "$K8S_ROOT" + apply_ollama_stack "$K8S_ROOT" "$K3S_ROOT" apply_operator_ai "$K8S_ROOT" fi +kubectl_cmd -n "$REAPERC2_NAMESPACE" rollout restart statefulset/mongo || true +kubectl_cmd -n "$REAPERC2_NAMESPACE" rollout status statefulset/mongo --timeout=600s || true rollout_reaperc2 "$REAPERC2_NAMESPACE" echo "Reroll complete." diff --git a/deployments/k3s/scripts/seed-mongo.sh b/deployments/k3s/scripts/seed-mongo.sh index 184da19..7a0a61d 100755 --- a/deployments/k3s/scripts/seed-mongo.sh +++ b/deployments/k3s/scripts/seed-mongo.sh @@ -20,6 +20,7 @@ export MONGO_API_USER="$app_user" export MONGO_API_PASSWORD="$app_pass" export MONGO_HOST="127.0.0.1" export MONGO_PORT="27017" +export MONGO_DATABASE="${MONGO_DATABASE:-reaperc2-metric}" export IMPORT_DATA_JSON="${IMPORT_DATA_JSON:-0}" echo "Port-forwarding MongoDB..." diff --git a/deployments/k3s/scripts/undeploy.sh b/deployments/k3s/scripts/undeploy.sh index e7a448a..136f327 100755 --- a/deployments/k3s/scripts/undeploy.sh +++ b/deployments/k3s/scripts/undeploy.sh @@ -26,18 +26,22 @@ while [[ $# -gt 0 ]]; do esac done +teardown_k3s_ingress "$K3S_ROOT" kubectl_cmd delete -f "${K3S_ROOT}/service.yaml" --ignore-not-found kubectl_cmd delete -f "${K3S_ROOT}/deployment.yaml" --ignore-not-found +kubectl_cmd delete configmap mongo-scripts mongo-init -n "$REAPERC2_NAMESPACE" --ignore-not-found kubectl_cmd delete -f "${K3S_ROOT}/mongodb.yaml" --ignore-not-found kubectl_cmd delete -f "${K3S_ROOT}/mongo-secret.yaml" --ignore-not-found +kubectl_cmd delete -f "${K3S_ROOT}/admin-bootstrap-secret.yaml" --ignore-not-found kubectl_cmd delete -f "${K8S_ROOT}/operator-ai.yaml" --ignore-not-found kubectl_cmd delete -f "${K8S_ROOT}/ollama.yaml" --ignore-not-found kubectl_cmd delete -f "${K3S_ROOT}/namespace.yaml" --ignore-not-found if [[ "$DELETE_PVC" == true ]]; then - kubectl_cmd delete pvc mongo-pvc -n reaperc2-ns --ignore-not-found + kubectl_cmd delete pvc mongo-pvc -n "$REAPERC2_NAMESPACE" --ignore-not-found kubectl_cmd delete pvc ollama-data -n ollama-ns --ignore-not-found kubectl_cmd delete pv reaperc2-mongo-nfs-pv --ignore-not-found + kubectl_cmd delete pv reaperc2-ollama-nfs-pv --ignore-not-found fi echo "Undeploy complete." diff --git a/deployments/k3s/scripts/verify-mongo-app-user.sh b/deployments/k3s/scripts/verify-mongo-app-user.sh new file mode 100755 index 0000000..5d04984 --- /dev/null +++ b/deployments/k3s/scripts/verify-mongo-app-user.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Verify app user can authenticate (reads credentials from mongo-secret.yaml). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "${SCRIPT_DIR}/lib.sh" +load_config + +MONGO_SECRET="${K3S_ROOT}/mongo-secret.yaml" +NS="${REAPERC2_NAMESPACE}" + +read_mongo_app_creds() { + python3 - <<'PY' "$MONGO_SECRET" +import sys +try: + import yaml +except ImportError: + print("PyYAML required: pip install pyyaml", file=sys.stderr) + sys.exit(2) +data = yaml.safe_load(open(sys.argv[1])) +s = data["stringData"] +print(s["app_username"]) +print(s["app_password"], end="") +PY +} + +mapfile -t CREDS < <(read_mongo_app_creds) +APP_USER="${CREDS[0]}" +APP_PASS="${CREDS[1]}" +DB="${MONGO_DATABASE:-reaperc2-metric}" + +echo "Verifying MongoDB app user ${APP_USER} on database ${DB}..." +if kubectl_cmd -n "$NS" exec statefulset/mongo -- mongosh --quiet \ + -u "$APP_USER" \ + -p "$APP_PASS" \ + --authenticationDatabase "$DB" \ + --eval 'db.runCommand({ ping: 1 }).ok'; then + echo "MongoDB app authentication OK." + exit 0 +fi + +echo "MongoDB app authentication failed." >&2 +exit 1 diff --git a/deployments/k3s/scripts/verify-mongo-secret.py b/deployments/k3s/scripts/verify-mongo-secret.py new file mode 100644 index 0000000..a43c800 --- /dev/null +++ b/deployments/k3s/scripts/verify-mongo-secret.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Validate mongo-secret.yaml app credentials (ReaperC2 uses discrete MONGO_* env, not a URI).""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("PyYAML required: pip install pyyaml", file=sys.stderr) + sys.exit(1) + + +def load_yaml(path: Path) -> dict: + data = yaml.safe_load(path.read_text()) + if not isinstance(data, dict): + raise SystemExit(f"Invalid YAML: {path}") + return data + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mongo-secret", + type=Path, + default=Path(__file__).resolve().parent.parent / "mongo-secret.yaml", + ) + args = parser.parse_args() + + mongo = load_yaml(args.mongo_secret) + string_data = mongo.get("stringData") or {} + + username = (string_data.get("app_username") or "").strip() + password = string_data.get("app_password") or "" + if not username or not password: + print("mongo-secret.yaml must define app_username and app_password", file=sys.stderr) + return 1 + + print(f"mongo-secret.yaml app user {username!r} is set.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/deployments/k3s/traefik-redirect-https-middleware.yaml b/deployments/k3s/traefik-redirect-https-middleware.yaml new file mode 100644 index 0000000..fca9856 --- /dev/null +++ b/deployments/k3s/traefik-redirect-https-middleware.yaml @@ -0,0 +1,9 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: reaperc2-ns +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/deployments/k3s/traefik-security-middleware.yaml b/deployments/k3s/traefik-security-middleware.yaml new file mode 100644 index 0000000..9f476e5 --- /dev/null +++ b/deployments/k3s/traefik-security-middleware.yaml @@ -0,0 +1,25 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: security-headers + namespace: reaperc2-ns +spec: + headers: + browserXssFilter: true + contentTypeNosniff: true + frameDeny: true + referrerPolicy: strict-origin-when-cross-origin + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: per-ip-ratelimit + namespace: reaperc2-ns +spec: + rateLimit: + average: 25 + period: 1s + burst: 50 diff --git a/deployments/k8s/ollama.yaml b/deployments/k8s/ollama.yaml index b2e9bbb..f1e10c2 100644 --- a/deployments/k8s/ollama.yaml +++ b/deployments/k8s/ollama.yaml @@ -22,7 +22,7 @@ metadata: data: # Comma-separated tags pulled on first pod start (empty = skip init pull). # Keep in sync with REAPER_AI_OLLAMA_MODELS in operator-ai.yaml when possible. - OLLAMA_PULL_MODELS: "llama3.2:latest,gpt-oss:latest,deepseek-r1:latest" + OLLAMA_PULL_MODELS: "llama3.2:latest,gpt-oss:latest,deepseek-r1:latest,glm-5.2:cloud" --- apiVersion: v1 kind: PersistentVolumeClaim @@ -82,6 +82,9 @@ spec: envFrom: - configMapRef: name: ollama-pull-models + - secretRef: + name: ollama-api-key + optional: true command: - /bin/sh - -c @@ -125,6 +128,10 @@ spec: # Listen on all interfaces inside the pod (ClusterIP Service only). - name: OLLAMA_HOST value: "0.0.0.0:11434" + envFrom: + - secretRef: + name: ollama-api-key + optional: true volumeMounts: - name: ollama-data mountPath: /root/.ollama diff --git a/deployments/k8s/operator-ai.yaml b/deployments/k8s/operator-ai.yaml index 99d9a55..01dcb71 100644 --- a/deployments/k8s/operator-ai.yaml +++ b/deployments/k8s/operator-ai.yaml @@ -64,7 +64,7 @@ data: REAPER_AI_OLLAMA_DISCOVER: "1" REAPER_AI_OLLAMA_API_URL: "http://ollama.ollama-ns.svc.cluster.local:11434/v1" # Comma-separated extras merged with live discovery from GET /api/tags: - REAPER_AI_OLLAMA_MODELS: "llama3.2:latest,gpt-oss:latest,deepseek-r1:latest" + REAPER_AI_OLLAMA_MODELS: "llama3.2:latest,gpt-oss:latest,deepseek-r1:latest,glm-5.2:cloud" # Host / Docker Compose only (replace API URL above): # REAPER_AI_OLLAMA_API_URL: "http://host.docker.internal:11434/v1" --- diff --git a/test/setup_mongo.sh b/test/setup_mongo.sh index e6d5d8d..5749e57 100755 --- a/test/setup_mongo.sh +++ b/test/setup_mongo.sh @@ -18,7 +18,7 @@ IMPORT_DATA_JSON="${IMPORT_DATA_JSON:-1}" DATA_JSON_COLLECTION="${DATA_JSON_COLLECTION:-seed_docs}" MONGO_URI="mongodb://${ADMINUSER}:${ADMINPASSWORD}@${MONGO_HOST}:${MONGO_PORT}" -DB_API_NAME="api_db" +DB_API_NAME="${MONGO_DATABASE:-api_db}" DB_DATA_COLLECTION="data" COLLECTION_CLIENTS="clients" COLLECTION_HEARTBEAT="heartbeat" @@ -113,15 +113,18 @@ EOF # mongosh "$MONGO_URI" --authenticationDatabase admin <