diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..3392041 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,131 @@ +name: GitHub Pages + +on: + push: + branches: + - main + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Create Helm packages directory + run: mkdir -p _site + + - name: Package Helm chart + run: | + helm package charts/nuclei-operator -d _site + + - name: Generate Helm repo index + run: | + helm repo index _site --url https://morten-olsen.github.io/homelab-nuclei-operator + + - name: Create index.html + run: | + cat > _site/index.html << 'EOF' + + + + + + Nuclei Operator Helm Repository + + + +

🔬 Nuclei Operator Helm Repository

+

+ This is the Helm chart repository for the + Nuclei Operator. +

+ +

Usage

+

Add this repository to Helm:

+
helm repo add nuclei-operator https://morten-olsen.github.io/homelab-nuclei-operator
+          helm repo update
+ +

Install the chart:

+
helm install nuclei-operator nuclei-operator/nuclei-operator \
+            --namespace nuclei-operator-system \
+            --create-namespace
+ +

Available Charts

+ + +

Links

+ + + + EOF + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..51359f8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,165 @@ +name: Release + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build and Push Container Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release-helm-chart: + name: Release Helm Chart + runs-on: ubuntu-latest + needs: build-and-push + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + pages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Update Chart version and appVersion + run: | + sed -i "s/^version:.*/version: ${{ steps.version.outputs.VERSION }}/" charts/nuclei-operator/Chart.yaml + sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.VERSION }}\"/" charts/nuclei-operator/Chart.yaml + + - name: Package Helm chart + run: | + helm package charts/nuclei-operator -d .helm-packages + + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + path: gh-pages + + - name: Update Helm repository + run: | + cp .helm-packages/*.tgz gh-pages/ + cd gh-pages + helm repo index . --url https://morten-olsen.github.io/homelab-nuclei-operator + git add . + git commit -m "Release Helm chart ${{ steps.version.outputs.VERSION }}" + git push + + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: build-and-push + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Install kustomize + run: | + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + sudo mv kustomize /usr/local/bin/ + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Generate install manifests + run: | + cd config/manager && kustomize edit set image controller=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + cd ../.. + kustomize build config/default > install.yaml + + - name: Package Helm chart + run: | + sed -i "s/^version:.*/version: ${{ steps.version.outputs.VERSION }}/" charts/nuclei-operator/Chart.yaml + sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.VERSION }}\"/" charts/nuclei-operator/Chart.yaml + helm package charts/nuclei-operator + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + install.yaml + nuclei-operator-*.tgz \ No newline at end of file diff --git a/README.md b/README.md index 121f5e1..c54f29a 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,71 @@ The Nuclei Operator watches for Ingress and VirtualService resources in your Kub ## Installation +### Using Helm (Recommended) + +The easiest way to install the Nuclei Operator is using Helm: + +```bash +# Add the Helm repository +helm repo add nuclei-operator https://morten-olsen.github.io/homelab-nuclei-operator +helm repo update + +# Install the operator +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace +``` + +#### Helm Configuration + +You can customize the installation by providing values: + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace \ + --set replicaCount=1 \ + --set resources.limits.memory=4Gi \ + --set nuclei.rescanAge=72h +``` + +Or create a `values.yaml` file: + +```yaml +replicaCount: 1 + +resources: + limits: + cpu: "2" + memory: "4Gi" + requests: + cpu: "500m" + memory: "1Gi" + +nuclei: + timeout: "1h" + rescanAge: "72h" + +serviceMonitor: + enabled: true +``` + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace \ + -f values.yaml +``` + +### Using Container Image from GitHub Container Registry + +The container images are available at: + +``` +ghcr.io/morten-olsen/homelab-nuclei-operator:latest +ghcr.io/morten-olsen/homelab-nuclei-operator:v0.1.0 # specific version +``` + ### Using kubectl/kustomize 1. **Install the CRDs:** @@ -63,7 +128,7 @@ make install ```bash # Using the default image -make deploy IMG=ghcr.io/mortenolsen/nuclei-operator:latest +make deploy IMG=ghcr.io/morten-olsen/homelab-nuclei-operator:latest # Or build and deploy your own image make docker-build docker-push IMG=/nuclei-operator:tag @@ -72,11 +137,18 @@ make deploy IMG=/nuclei-operator:tag ### Using a Single YAML File -Generate and apply a consolidated installation manifest: +Download and apply the installation manifest from the GitHub release: + +```bash +# Download from the latest release +kubectl apply -f https://github.com/morten-olsen/homelab-nuclei-operator/releases/latest/download/install.yaml +``` + +Or generate it locally: ```bash # Generate the installer -make build-installer IMG=/nuclei-operator:tag +make build-installer IMG=ghcr.io/morten-olsen/homelab-nuclei-operator:latest # Apply to your cluster kubectl apply -f dist/install.yaml @@ -86,8 +158,8 @@ kubectl apply -f dist/install.yaml ```bash # Clone the repository -git clone https://github.com/mortenolsen/nuclei-operator.git -cd nuclei-operator +git clone https://github.com/morten-olsen/homelab-nuclei-operator.git +cd homelab-nuclei-operator # Build the binary make build @@ -103,8 +175,20 @@ make docker-push IMG=/nuclei-operator:tag ### 1. Deploy the Operator +Using Helm (recommended): + ```bash -make deploy IMG=ghcr.io/mortenolsen/nuclei-operator:latest +helm repo add nuclei-operator https://morten-olsen.github.io/homelab-nuclei-operator +helm repo update +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace +``` + +Or using kustomize: + +```bash +make deploy IMG=ghcr.io/morten-olsen/homelab-nuclei-operator:latest ``` ### 2. Create an Ingress Resource @@ -371,6 +455,21 @@ curl localhost:8080/metrics ## Uninstallation +### Using Helm + +```bash +# Uninstall the operator +helm uninstall nuclei-operator -n nuclei-operator-system + +# Remove the namespace (optional) +kubectl delete namespace nuclei-operator-system + +# Remove CRDs (optional - this will delete all NucleiScan resources) +kubectl delete crd nucleiscans.nuclei.homelab.mortenolsen.pro +``` + +### Using kubectl/kustomize + ```bash # Remove all NucleiScan resources kubectl delete nucleiscans --all --all-namespaces diff --git a/charts/nuclei-operator/.helmignore b/charts/nuclei-operator/.helmignore new file mode 100644 index 0000000..691fa13 --- /dev/null +++ b/charts/nuclei-operator/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ \ No newline at end of file diff --git a/charts/nuclei-operator/Chart.yaml b/charts/nuclei-operator/Chart.yaml new file mode 100644 index 0000000..3daaf59 --- /dev/null +++ b/charts/nuclei-operator/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: nuclei-operator +description: A Kubernetes operator that automatically scans Ingress and VirtualService resources using Nuclei security scanner +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/morten-olsen/homelab-nuclei-operator +sources: + - https://github.com/morten-olsen/homelab-nuclei-operator +maintainers: + - name: Morten Olsen + url: https://github.com/morten-olsen +keywords: + - kubernetes + - operator + - security + - nuclei + - scanner + - ingress + - virtualservice +kubeVersion: ">=1.26.0-0" \ No newline at end of file diff --git a/charts/nuclei-operator/README.md b/charts/nuclei-operator/README.md new file mode 100644 index 0000000..0a66621 --- /dev/null +++ b/charts/nuclei-operator/README.md @@ -0,0 +1,233 @@ +# Nuclei Operator Helm Chart + +A Helm chart for deploying the Nuclei Operator - a Kubernetes operator that automatically scans Ingress and VirtualService resources using Nuclei security scanner. + +## Prerequisites + +- Kubernetes 1.26+ +- Helm 3.0+ + +## Installation + +### Add the Helm Repository + +```bash +helm repo add nuclei-operator https://morten-olsen.github.io/homelab-nuclei-operator +helm repo update +``` + +### Install the Chart + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace +``` + +### Install with Custom Values + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace \ + -f values.yaml +``` + +## Configuration + +The following table lists the configurable parameters of the Nuclei Operator chart and their default values. + +### General + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas | `1` | +| `nameOverride` | Override the name of the chart | `""` | +| `fullnameOverride` | Override the full name of the chart | `""` | + +### Image + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.repository` | Container image repository | `ghcr.io/morten-olsen/homelab-nuclei-operator` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag (defaults to chart appVersion) | `""` | +| `imagePullSecrets` | Image pull secrets | `[]` | + +### Service Account + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `serviceAccount.create` | Create a service account | `true` | +| `serviceAccount.annotations` | Service account annotations | `{}` | +| `serviceAccount.name` | Service account name | `""` | + +### Pod Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `podAnnotations` | Pod annotations | `{}` | +| `podLabels` | Pod labels | `{}` | +| `podSecurityContext.runAsNonRoot` | Run as non-root | `true` | +| `podSecurityContext.seccompProfile.type` | Seccomp profile type | `RuntimeDefault` | + +### Container Security Context + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `securityContext.readOnlyRootFilesystem` | Read-only root filesystem | `false` | +| `securityContext.allowPrivilegeEscalation` | Allow privilege escalation | `false` | +| `securityContext.runAsNonRoot` | Run as non-root | `true` | +| `securityContext.runAsUser` | User ID | `65532` | +| `securityContext.capabilities.drop` | Dropped capabilities | `["ALL"]` | + +### Resources + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `resources.limits.cpu` | CPU limit | `"2"` | +| `resources.limits.memory` | Memory limit | `"2Gi"` | +| `resources.requests.cpu` | CPU request | `"500m"` | +| `resources.requests.memory` | Memory request | `"512Mi"` | + +### Scheduling + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nodeSelector` | Node selector | `{}` | +| `tolerations` | Tolerations | `[]` | +| `affinity` | Affinity rules | `{}` | + +### Leader Election + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `leaderElection.enabled` | Enable leader election | `true` | + +### Health Probes + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `healthProbes.livenessProbe.httpGet.path` | Liveness probe path | `/healthz` | +| `healthProbes.livenessProbe.httpGet.port` | Liveness probe port | `8081` | +| `healthProbes.livenessProbe.initialDelaySeconds` | Initial delay | `15` | +| `healthProbes.livenessProbe.periodSeconds` | Period | `20` | +| `healthProbes.readinessProbe.httpGet.path` | Readiness probe path | `/readyz` | +| `healthProbes.readinessProbe.httpGet.port` | Readiness probe port | `8081` | +| `healthProbes.readinessProbe.initialDelaySeconds` | Initial delay | `5` | +| `healthProbes.readinessProbe.periodSeconds` | Period | `10` | + +### Metrics + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `metrics.enabled` | Enable metrics endpoint | `true` | +| `metrics.service.type` | Metrics service type | `ClusterIP` | +| `metrics.service.port` | Metrics service port | `8443` | + +### Nuclei Scanner Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nuclei.binaryPath` | Path to nuclei binary | `/usr/local/bin/nuclei` | +| `nuclei.templatesPath` | Path to nuclei templates | `/nuclei-templates` | +| `nuclei.timeout` | Scan timeout | `30m` | +| `nuclei.rescanAge` | Age before automatic rescan | `168h` | +| `nuclei.backoff.initial` | Initial backoff interval | `10s` | +| `nuclei.backoff.max` | Maximum backoff interval | `10m` | +| `nuclei.backoff.multiplier` | Backoff multiplier | `2.0` | + +### ServiceMonitor (Prometheus Operator) + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `serviceMonitor.enabled` | Enable ServiceMonitor | `false` | +| `serviceMonitor.labels` | Additional labels | `{}` | +| `serviceMonitor.interval` | Scrape interval | `30s` | +| `serviceMonitor.scrapeTimeout` | Scrape timeout | `10s` | + +### Network Policy + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `networkPolicy.enabled` | Enable network policy | `false` | + +## Examples + +### Basic Installation + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace +``` + +### With Prometheus Monitoring + +```yaml +# values.yaml +metrics: + enabled: true + +serviceMonitor: + enabled: true + labels: + release: prometheus +``` + +```bash +helm install nuclei-operator nuclei-operator/nuclei-operator \ + --namespace nuclei-operator-system \ + --create-namespace \ + -f values.yaml +``` + +### With Custom Resource Limits + +```yaml +# values.yaml +resources: + limits: + cpu: "4" + memory: "4Gi" + requests: + cpu: "1" + memory: "1Gi" + +nuclei: + timeout: "1h" + rescanAge: "24h" +``` + +### With Node Affinity + +```yaml +# values.yaml +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 +``` + +## Uninstallation + +```bash +helm uninstall nuclei-operator -n nuclei-operator-system +``` + +To also remove the CRDs: + +```bash +kubectl delete crd nucleiscans.nuclei.homelab.mortenolsen.pro +``` + +## Links + +- [GitHub Repository](https://github.com/morten-olsen/homelab-nuclei-operator) +- [Nuclei Scanner](https://github.com/projectdiscovery/nuclei) \ No newline at end of file diff --git a/charts/nuclei-operator/templates/NOTES.txt b/charts/nuclei-operator/templates/NOTES.txt new file mode 100644 index 0000000..17fe52d --- /dev/null +++ b/charts/nuclei-operator/templates/NOTES.txt @@ -0,0 +1,44 @@ +Thank you for installing {{ .Chart.Name }}! + +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} + $ helm get all {{ .Release.Name }} + +The nuclei-operator is now watching for Ingress and VirtualService resources. + +To create a NucleiScan manually, you can apply a resource like: + + apiVersion: nuclei.homelab.mortenolsen.pro/v1alpha1 + kind: NucleiScan + metadata: + name: example-scan + spec: + sourceRef: + apiVersion: networking.k8s.io/v1 + kind: Ingress + name: my-ingress + namespace: default + uid: + targets: + - https://example.com + +To view scan results: + + $ kubectl get nucleiscans -A + $ kubectl describe nucleiscan + +{{- if .Values.metrics.enabled }} + +Metrics are enabled. The metrics service is available at: + {{ include "nuclei-operator.fullname" . }}-metrics-service:{{ .Values.metrics.service.port }} + +{{- if .Values.serviceMonitor.enabled }} +A ServiceMonitor has been created for Prometheus Operator integration. +{{- end }} +{{- end }} + +For more information, visit: + https://github.com/morten-olsen/homelab-nuclei-operator \ No newline at end of file diff --git a/charts/nuclei-operator/templates/_helpers.tpl b/charts/nuclei-operator/templates/_helpers.tpl new file mode 100644 index 0000000..1c1be69 --- /dev/null +++ b/charts/nuclei-operator/templates/_helpers.tpl @@ -0,0 +1,71 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nuclei-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nuclei-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nuclei-operator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nuclei-operator.labels" -}} +helm.sh/chart: {{ include "nuclei-operator.chart" . }} +{{ include "nuclei-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nuclei-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nuclei-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +control-plane: controller-manager +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nuclei-operator.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nuclei-operator.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the image name +*/}} +{{- define "nuclei-operator.image" -}} +{{- $tag := default .Chart.AppVersion .Values.image.tag }} +{{- printf "%s:%s" .Values.image.repository $tag }} +{{- end }} \ No newline at end of file diff --git a/charts/nuclei-operator/templates/crds/nucleiscan-crd.yaml b/charts/nuclei-operator/templates/crds/nucleiscan-crd.yaml new file mode 100644 index 0000000..11bc39c --- /dev/null +++ b/charts/nuclei-operator/templates/crds/nucleiscan-crd.yaml @@ -0,0 +1,322 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: nucleiscans.nuclei.homelab.mortenolsen.pro + labels: { { - include "nuclei-operator.labels" . | nindent 4 } } +spec: + group: nuclei.homelab.mortenolsen.pro + names: + kind: NucleiScan + listKind: NucleiScanList + plural: nucleiscans + shortNames: + - ns + - nscan + singular: nucleiscan + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.summary.totalFindings + name: Findings + type: integer + - jsonPath: .spec.sourceRef.kind + name: Source + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: NucleiScan is the Schema for the nucleiscans API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: NucleiScanSpec defines the desired state of NucleiScan + properties: + schedule: + description: |- + Schedule for periodic rescanning in cron format + If empty, scan runs once + type: string + severity: + description: Severity filters scan results by severity level + items: + type: string + type: array + sourceRef: + description: + SourceRef references the Ingress or VirtualService being + scanned + properties: + apiVersion: + description: APIVersion of the source resource + type: string + kind: + description: Kind of the source resource - Ingress or VirtualService + enum: + - Ingress + - VirtualService + type: string + name: + description: Name of the source resource + type: string + namespace: + description: Namespace of the source resource + type: string + uid: + description: UID of the source resource for owner reference + type: string + required: + - apiVersion + - kind + - name + - namespace + - uid + type: object + suspend: + description: Suspend prevents scheduled scans from running + type: boolean + targets: + description: + Targets is the list of URLs to scan, extracted from the + source resource + items: + type: string + minItems: 1 + type: array + templates: + description: |- + Templates specifies which Nuclei templates to use + If empty, uses default templates + items: + type: string + type: array + required: + - sourceRef + - targets + type: object + status: + description: NucleiScanStatus defines the observed state of NucleiScan + properties: + completionTime: + description: CompletionTime is when the last scan completed + format: date-time + type: string + conditions: + description: Conditions represent the latest available observations + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + findings: + description: |- + Findings contains the array of scan results from Nuclei JSONL output + Each element is a parsed JSON object from Nuclei output + items: + description: Finding represents a single Nuclei scan finding + properties: + description: + description: Description provides details about the finding + type: string + extractedResults: + description: + ExtractedResults contains any data extracted by + the template + items: + type: string + type: array + host: + description: Host that was scanned + type: string + matchedAt: + description: + MatchedAt is the specific URL or endpoint where + the issue was found + type: string + metadata: + description: Metadata contains additional template metadata + type: object + x-kubernetes-preserve-unknown-fields: true + reference: + description: + Reference contains URLs to additional information + about the finding + items: + type: string + type: array + severity: + description: Severity of the finding + type: string + tags: + description: Tags associated with the finding + items: + type: string + type: array + templateId: + description: TemplateID is the Nuclei template identifier + type: string + templateName: + description: TemplateName is the human-readable template name + type: string + timestamp: + description: Timestamp when the finding was discovered + format: date-time + type: string + type: + description: Type of the finding - http, dns, ssl, etc. + type: string + required: + - host + - severity + - templateId + - timestamp + type: object + type: array + lastError: + description: LastError contains the error message if the scan failed + type: string + lastRetryTime: + description: + LastRetryTime is when the last availability check retry + occurred + format: date-time + type: string + lastScanTime: + description: LastScanTime is when the last scan was initiated + format: date-time + type: string + nextScheduledTime: + description: + NextScheduledTime is when the next scheduled scan will + run + format: date-time + type: string + observedGeneration: + description: + ObservedGeneration is the generation observed by the + controller + format: int64 + type: integer + phase: + description: Phase represents the current scan phase + enum: + - Pending + - Running + - Completed + - Failed + type: string + retryCount: + description: |- + RetryCount tracks the number of consecutive availability check retries + Used for exponential backoff when waiting for targets + type: integer + summary: + description: Summary provides aggregated scan statistics + properties: + durationSeconds: + description: DurationSeconds is the duration of the scan in seconds + format: int64 + type: integer + findingsBySeverity: + additionalProperties: + type: integer + description: + FindingsBySeverity breaks down findings by severity + level + type: object + targetsScanned: + description: + TargetsScanned is the number of targets that were + scanned + type: integer + totalFindings: + description: TotalFindings is the total number of findings + type: integer + required: + - targetsScanned + - totalFindings + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/nuclei-operator/templates/deployment.yaml b/charts/nuclei-operator/templates/deployment.yaml new file mode 100644 index 0000000..72e7044 --- /dev/null +++ b/charts/nuclei-operator/templates/deployment.yaml @@ -0,0 +1,99 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nuclei-operator.fullname" . }}-controller-manager + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "nuclei-operator.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nuclei-operator.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nuclei-operator.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: manager + image: {{ include "nuclei-operator.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /manager + args: + {{- if .Values.leaderElection.enabled }} + - --leader-elect + {{- end }} + - --health-probe-bind-address=:8081 + {{- if .Values.metrics.enabled }} + - --metrics-bind-address=:8443 + - --metrics-secure=true + {{- end }} + env: + - name: NUCLEI_BINARY_PATH + value: {{ .Values.nuclei.binaryPath | quote }} + - name: NUCLEI_TEMPLATES_PATH + value: {{ .Values.nuclei.templatesPath | quote }} + - name: NUCLEI_TIMEOUT + value: {{ .Values.nuclei.timeout | quote }} + - name: NUCLEI_RESCAN_AGE + value: {{ .Values.nuclei.rescanAge | quote }} + - name: NUCLEI_BACKOFF_INITIAL + value: {{ .Values.nuclei.backoff.initial | quote }} + - name: NUCLEI_BACKOFF_MAX + value: {{ .Values.nuclei.backoff.max | quote }} + - name: NUCLEI_BACKOFF_MULTIPLIER + value: {{ .Values.nuclei.backoff.multiplier | quote }} + ports: [] + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + {{- with .Values.healthProbes.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.healthProbes.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + - name: nuclei-templates + mountPath: {{ .Values.nuclei.templatesPath }} + readOnly: true + - name: nuclei-cache + mountPath: /home/nonroot/.nuclei + volumes: + - name: nuclei-templates + emptyDir: {} + - name: nuclei-cache + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/charts/nuclei-operator/templates/metrics-service.yaml b/charts/nuclei-operator/templates/metrics-service.yaml new file mode 100644 index 0000000..3533201 --- /dev/null +++ b/charts/nuclei-operator/templates/metrics-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nuclei-operator.fullname" . }}-metrics-service + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.metrics.service.type }} + ports: + - name: https + port: {{ .Values.metrics.service.port }} + protocol: TCP + targetPort: 8443 + selector: + {{- include "nuclei-operator.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/nuclei-operator/templates/rbac.yaml b/charts/nuclei-operator/templates/rbac.yaml new file mode 100644 index 0000000..341ef3c --- /dev/null +++ b/charts/nuclei-operator/templates/rbac.yaml @@ -0,0 +1,192 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "nuclei-operator.fullname" . }}-manager-role + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - networking.istio.io + resources: + - virtualservices + verbs: + - get + - list + - watch +- apiGroups: + - networking.istio.io + resources: + - virtualservices/status + verbs: + - get +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - get +- apiGroups: + - nuclei.homelab.mortenolsen.pro + resources: + - nucleiscans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nuclei.homelab.mortenolsen.pro + resources: + - nucleiscans/finalizers + verbs: + - update +- apiGroups: + - nuclei.homelab.mortenolsen.pro + resources: + - nucleiscans/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "nuclei-operator.fullname" . }}-manager-rolebinding + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "nuclei-operator.fullname" . }}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "nuclei-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +# Leader election role +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "nuclei-operator.fullname" . }}-leader-election-role + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "nuclei-operator.fullname" . }}-leader-election-rolebinding + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "nuclei-operator.fullname" . }}-leader-election-role +subjects: +- kind: ServiceAccount + name: {{ include "nuclei-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- if .Values.metrics.enabled }} +--- +# Metrics auth role +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "nuclei-operator.fullname" . }}-metrics-auth-role + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "nuclei-operator.fullname" . }}-metrics-auth-rolebinding + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "nuclei-operator.fullname" . }}-metrics-auth-role +subjects: +- kind: ServiceAccount + name: {{ include "nuclei-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +# Metrics reader role +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "nuclei-operator.fullname" . }}-metrics-reader + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +{{- end }} \ No newline at end of file diff --git a/charts/nuclei-operator/templates/serviceaccount.yaml b/charts/nuclei-operator/templates/serviceaccount.yaml new file mode 100644 index 0000000..ffaf1d5 --- /dev/null +++ b/charts/nuclei-operator/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nuclei-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/nuclei-operator/templates/servicemonitor.yaml b/charts/nuclei-operator/templates/servicemonitor.yaml new file mode 100644 index 0000000..cc071df --- /dev/null +++ b/charts/nuclei-operator/templates/servicemonitor.yaml @@ -0,0 +1,25 @@ +{{- if and .Values.metrics.enabled .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "nuclei-operator.fullname" . }}-metrics-monitor + namespace: {{ .Release.Namespace }} + labels: + {{- include "nuclei-operator.labels" . | nindent 4 }} + {{- with .Values.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + interval: {{ .Values.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + {{- include "nuclei-operator.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/charts/nuclei-operator/values.yaml b/charts/nuclei-operator/values.yaml new file mode 100644 index 0000000..3bc1e17 --- /dev/null +++ b/charts/nuclei-operator/values.yaml @@ -0,0 +1,133 @@ +# Default values for nuclei-operator. + +# Number of replicas for the controller manager +replicaCount: 1 + +image: + # Container image repository + repository: ghcr.io/morten-olsen/homelab-nuclei-operator + # Image pull policy + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion + tag: "" + +# Image pull secrets for private registries +imagePullSecrets: [] + +# Override the name of the chart +nameOverride: "" +# Override the full name of the chart +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# Pod annotations +podAnnotations: {} + +# Pod labels +podLabels: {} + +# Pod security context +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +# Container security context +securityContext: + readOnlyRootFilesystem: false # Nuclei needs to write temporary files + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65532 + capabilities: + drop: + - ALL + +# Resource limits and requests +resources: + limits: + cpu: "2" + memory: "2Gi" + requests: + cpu: "500m" + memory: "512Mi" + +# Node selector for pod scheduling +nodeSelector: {} + +# Tolerations for pod scheduling +tolerations: [] + +# Affinity rules for pod scheduling +affinity: {} + +# Leader election configuration +leaderElection: + enabled: true + +# Health probe configuration +healthProbes: + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +# Metrics configuration +metrics: + # Enable metrics endpoint + enabled: true + # Service configuration for metrics + service: + type: ClusterIP + port: 8443 + +# Nuclei scanner configuration +nuclei: + # Path to nuclei binary inside the container + binaryPath: "/usr/local/bin/nuclei" + # Path to nuclei templates + templatesPath: "/nuclei-templates" + # Scan timeout duration + timeout: "30m" + # Rescan age - how old scan results can be before triggering automatic rescan + # Set to "0" to disable automatic rescans based on age + rescanAge: "168h" + # Backoff configuration for target availability checks + backoff: + # Initial retry interval + initial: "10s" + # Maximum retry interval + max: "10m" + # Multiplier for exponential backoff + multiplier: "2.0" + +# ServiceMonitor for Prometheus Operator +serviceMonitor: + # Enable ServiceMonitor creation + enabled: false + # Additional labels for the ServiceMonitor + labels: {} + # Scrape interval + interval: 30s + # Scrape timeout + scrapeTimeout: 10s + +# Network policies +networkPolicy: + # Enable network policy + enabled: false \ No newline at end of file