diff --git a/charts/compliance-scanning/Chart.yaml b/charts/compliance-scanning/Chart.yaml new file mode 100644 index 00000000..270a1fb8 --- /dev/null +++ b/charts/compliance-scanning/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +description: compliance-scanning performs hardening of OCP cluster vis predefiend profile(s) +keywords: + - pattern + - compliance + - zero trust + - hardening +name: compliance-scanning +type: application +icon: https://validatedpatterns.io/images/validated-patterns.png +version: 0.0.3 diff --git a/charts/compliance-scanning/templates/_helpers.tpl b/charts/compliance-scanning/templates/_helpers.tpl new file mode 100644 index 00000000..a83fcdf8 --- /dev/null +++ b/charts/compliance-scanning/templates/_helpers.tpl @@ -0,0 +1,61 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "compliance-scanning.name" -}} +{{- default .Chart.Name .Values.nameOverride }} +{{- end }} + +{{/* +Create a default fully qualified app name. +If release name contains chart name it will be used as a full name. +*/}} +{{- define "compliance-scanning.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "compliance-scanning.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "compliance-scanning.labels" -}} +helm.sh/chart: {{ include "compliance-scanning.chart" . }} +{{ include "compliance-scanning.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "compliance-scanning.selectorLabels" -}} +app.kubernetes.io/name: {{ include "compliance-scanning.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "compliance-scanning.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "compliance-scanning.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/compliance-scanning/templates/pvc.yaml b/charts/compliance-scanning/templates/pvc.yaml new file mode 100644 index 00000000..4c7ca393 --- /dev/null +++ b/charts/compliance-scanning/templates/pvc.yaml @@ -0,0 +1,20 @@ +{{- if .Values.compliance.storage.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.compliance.storage.pvc.name }} + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '-10' + labels: + app.kubernetes.io/component: storage +spec: + accessModes: + - {{ .Values.compliance.storage.pvc.accessMode }} + resources: + requests: + storage: {{ .Values.compliance.storage.pvc.size }} + {{- if .Values.compliance.storage.pvc.storageClass }} + storageClassName: {{ .Values.compliance.storage.pvc.storageClass }} + {{- end }} +{{- end }} diff --git a/charts/compliance-scanning/templates/remediation-job.yaml b/charts/compliance-scanning/templates/remediation-job.yaml new file mode 100644 index 00000000..85e395bf --- /dev/null +++ b/charts/compliance-scanning/templates/remediation-job.yaml @@ -0,0 +1,206 @@ +# ServiceAccount for the remediation job +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.compliance.remediationJob.serviceAccount.name }} + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '0' + labels: + app.kubernetes.io/component: compliance-remediation-job +--- +# ClusterRole with permissions to manage compliance resources +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.compliance.remediationJob.clusterRole.name }} + annotations: + argocd.argoproj.io/sync-wave: '0' + labels: + app.kubernetes.io/component: compliance-remediation-job +rules: +- apiGroups: ["compliance.openshift.io"] + resources: ["compliancescans"] + verbs: ["get", "list", "watch"] +- apiGroups: ["compliance.openshift.io"] + resources: ["complianceremediations"] + verbs: ["get", "list", "watch", "update", "patch"] +- apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] +--- +# ClusterRoleBinding to bind the role to the service account +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.compliance.remediationJob.clusterRoleBinding.name }} + annotations: + argocd.argoproj.io/sync-wave: '0' + labels: + app.kubernetes.io/component: compliance-remediation-job +subjects: +- kind: ServiceAccount + name: {{ .Values.compliance.remediationJob.serviceAccount.name }} + namespace: openshift-compliance +roleRef: + kind: ClusterRole + name: {{ .Values.compliance.remediationJob.clusterRole.name }} + apiGroup: rbac.authorization.k8s.io +--- +# Job to wait for scan completion and apply remediations +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Values.compliance.remediationJob.name }} + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '10' + labels: + app.kubernetes.io/component: compliance-remediation-job +spec: + template: + metadata: + labels: + app.kubernetes.io/component: compliance-remediation-job + spec: + serviceAccountName: {{ .Values.compliance.remediationJob.serviceAccount.name }} + restartPolicy: OnFailure + containers: + - name: remediation-applier + image: {{ .Values.compliance.remediationJob.image }} + command: ["/bin/bash"] + args: + - -c + - | + set -Eeuxo pipefail + + echo "Starting compliance remediation job..." + echo "Scan Setting Binding: {{ .Values.compliance.scanSettingBinding.name }}" + echo "Profile: {{ .Values.compliance.scanSettingBinding.profile }}" + + # The ComplianceScan name is typically generated from the binding name and profile + # Format: - + SCAN_NAME="{{ .Values.compliance.scanSettingBinding.profile }}-worker" + NAMESPACE="openshift-compliance" + + echo "Waiting for ComplianceScan: $SCAN_NAME to complete..." + + # Wait for the scan to exist first + + max_wait_for_scan=600 # 10 minutes + + wait_time=0 + + + while [[ $(kubectl get compliancescan "$SCAN_NAME" -n "$NAMESPACE" -o \ + jsonpath='{..status.conditions[?(@.type=="Ready")].status}') != "True" ]]; do + if [ $wait_time -ge $max_wait_for_scan ]; then + echo "ERROR: ComplianceScan $SCAN_NAME not found after ${max_wait_for_scan}s" + exit 1 + fi + echo "Waiting for ComplianceScan $SCAN_NAME to be created... (${wait_time}s elapsed)" + sleep 10 + wait_time=$((wait_time + 10)) + done + + + echo "ComplianceScan $SCAN_NAME found. Waiting for completion..." + + + # Wait for the scan to complete + + max_wait=7200 # 2 hours + + wait_time=0 + + + while true; do + # Get the scan phase/status + PHASE=$(kubectl get compliancescan "$SCAN_NAME" -n "$NAMESPACE" -o jsonpath='{.status.phase}') + + if [ "$PHASE" = "DONE" ]; then + echo "ComplianceScan $SCAN_NAME completed successfully!" + break + elif [ "$PHASE" = "ERROR" ] || [ "$PHASE" = "FAILED" ]; then + echo "ERROR: ComplianceScan $SCAN_NAME failed with phase: $PHASE" + exit 1 + elif [ -z "$PHASE" ]; then + echo "ComplianceScan $SCAN_NAME phase is empty, waiting..." + else + echo "ComplianceScan $SCAN_NAME is in phase: $PHASE (waiting...)" + fi + + if [ $wait_time -ge $max_wait ]; then + echo "ERROR: ComplianceScan $SCAN_NAME did not complete within ${max_wait}s" + exit 1 + fi + + sleep 30 + wait_time=$((wait_time + 30)) + done + + + echo "Scan completed. Looking for pending ComplianceRemediations..." + + + # Get all ComplianceRemediation objects with state=Pending and spec.apply=false + + PENDING_REMEDIATIONS=$(kubectl get complianceremediations -n "$NAMESPACE" -o json | \ + jq -r '.items[] | select(.status.applicationState == "Pending" and .spec.apply == false) | .metadata.name') + + if [ -z "$PENDING_REMEDIATIONS" ]; then + echo "No pending ComplianceRemediations found with spec.apply=false" + exit 0 + fi + + + echo "Found pending ComplianceRemediations to apply:" + + echo "$PENDING_REMEDIATIONS" + + + # Apply each remediation by setting spec.apply to true + + APPLIED_COUNT=0 + + for REMEDIATION in $PENDING_REMEDIATIONS; do + echo "Applying remediation: $REMEDIATION" + + if kubectl patch complianceremediation "$REMEDIATION" -n "$NAMESPACE" \ + --type='merge' \ + --patch='{"spec": {"apply": true}}'; then + echo "Successfully applied remediation: $REMEDIATION" + APPLIED_COUNT=$((APPLIED_COUNT + 1)) + else + echo "ERROR: Failed to apply remediation: $REMEDIATION" + fi + done + + + echo "Remediation job completed. Applied $APPLIED_COUNT remediations." + + + # Create an event to record the action + + kubectl create event \ + --namespace="$NAMESPACE" \ + --message="Applied $APPLIED_COUNT ComplianceRemediations after scan completion" \ + --for="job/compliance-remediation-job" || true + resources: + {{- toYaml .Values.compliance.remediationJob.resources | nindent 10 }} + {{- if .Values.compliance.remediationJob.securityContext }} + securityContext: + {{- toYaml .Values.compliance.remediationJob.securityContext | nindent 10 }} + {{- end }} + {{- if .Values.compliance.remediationJob.nodeSelector }} + nodeSelector: + {{- toYaml .Values.compliance.remediationJob.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.compliance.remediationJob.tolerations }} + tolerations: + {{- toYaml .Values.compliance.remediationJob.tolerations | nindent 8 }} + {{- end }} + {{- if .Values.compliance.remediationJob.affinity }} + affinity: + {{- toYaml .Values.compliance.remediationJob.affinity | nindent 8 }} + {{- end }} diff --git a/charts/compliance-scanning/templates/scan-setting-binding.yaml b/charts/compliance-scanning/templates/scan-setting-binding.yaml new file mode 100644 index 00000000..20d966be --- /dev/null +++ b/charts/compliance-scanning/templates/scan-setting-binding.yaml @@ -0,0 +1,19 @@ +apiVersion: compliance.openshift.io/v1alpha1 +kind: ScanSettingBinding +metadata: + name: {{ .Values.compliance.scanSettingBinding.name }} + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '-10' + labels: + app.kubernetes.io/component: compliance-scan-binding +settingsRef: + kind: ScanSetting + apiGroup: compliance.openshift.io/v1alpha1 + name: {{ .Values.compliance.scanSetting.name }} +# Profiles to bind to this scan setting +profiles: + - name: {{ .Values.compliance.scanSettingBinding.profile }} + kind: Profile + apiGroup: compliance.openshift.io/v1alpha1 + diff --git a/charts/compliance-scanning/templates/scan-setting.yaml b/charts/compliance-scanning/templates/scan-setting.yaml new file mode 100644 index 00000000..3c891905 --- /dev/null +++ b/charts/compliance-scanning/templates/scan-setting.yaml @@ -0,0 +1,39 @@ +apiVersion: compliance.openshift.io/v1alpha1 +kind: ScanSetting +metadata: + name: {{ .Values.compliance.scanSetting.name }} + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '-10' + labels: + app.kubernetes.io/component: compliance-scan-setting +# Enable automatic application of remediations +autoApplyRemediations: {{ .Values.compliance.scanSetting.autoApplyRemediations }} +# Use default-auto-apply annotation for automatic remediation +autoUpdateRemediations: true +# Scanning schedule (empty means manual trigger) +{{- if .Values.compliance.scanSetting.schedule }} +schedule: {{ .Values.compliance.scanSetting.schedule | quote }} +{{- end }} +roles: + - master + - worker +# Scanner pod tolerations to run on all nodes including masters +scanTolerations: + {{- toYaml .Values.compliance.scanSetting.scanTolerations | nindent 4 }} +# Node selector for scanner pods (if specified) +{{- if .Values.compliance.scanSetting.nodeSelector }} +nodeSelector: + {{- toYaml .Values.compliance.scanSetting.nodeSelector | nindent 4 }} +{{- end }} +# PVC for storing scan results +{{- if .Values.compliance.storage.enabled }} +rawResultStorage: + pvAccessModes: + - {{ .Values.compliance.storage.pvc.accessMode }} + rotation: 5 + size: {{ .Values.compliance.storage.pvc.size }} + {{- if .Values.compliance.storage.pvc.storageClass }} + storageClassName: {{ .Values.compliance.storage.pvc.storageClass }} + {{- end }} +{{- end }} diff --git a/charts/compliance-scanning/values.yaml b/charts/compliance-scanning/values.yaml new file mode 100644 index 00000000..de7b9789 --- /dev/null +++ b/charts/compliance-scanning/values.yaml @@ -0,0 +1,60 @@ +global: + localClusterDomain: local.example.com + +compliance: + # ScanSetting configuration + scanSetting: + name: compliance-scan-setting + # Enable automatic remediation + autoApplyRemediations: true + # Schedule scan + schedule: "" + # Set scanning tolerations + scanTolerations: + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + # Node selector for scanner pods + nodeSelector: {} + # (Future look at creating a TailoredProfile for ZT) + scanSettingBinding: + name: compliance-scan-binding + # Profile in oc get profiles.compliance -n openshift-compliance + profile: "rhcos4-stig" + # Storage configuration for scan results + storage: + # Enable PVC creation + enabled: true + pvc: + name: compliance-scan-results + storageClass: "" + size: "2Gi" + accessMode: "ReadWriteOnce" + # Remediation job configuration + remediationJob: + # Job name + name: compliance-remediation-job + # Container image with kubectl and jq + image: "quay.io/hybridcloudpatterns/imperative-container:v1" + # Service account configuration + serviceAccount: + name: compliance-remediation-sa + # RBAC configuration + clusterRole: + name: compliance-remediation-role + clusterRoleBinding: + name: compliance-remediation-binding + # Resource limits and requests + resources: + limits: + cpu: "200m" + memory: "256Mi" + requests: + cpu: "100m" + memory: "128Mi" + # Node selector (optional) + nodeSelector: {} + # Tolerations (optional) + tolerations: [] + # Affinity (optional) + affinity: {} diff --git a/values-hub.yaml b/values-hub.yaml index 33e48e5d..279f4a4d 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -78,9 +78,18 @@ clusterGroup: # - '/overrides/values-{{ $.Values.global.hubClusterDomain }}.yaml' # - '/overrides/values-{{ $.Values.global.localClusterDomain }}.yaml' applications: + compliance-scanning: + name: compliance-scanning + namespace: openshift-compliance + annotations: + argocd.argoproj.io/sync-wave: '-30' + project: hub + path: charts/compliance-scanning vault: name: vault namespace: vault + annotations: + argocd.argoproj.io/sync-wave: '-20' project: hub chart: hashicorp-vault chartVersion: 0.1.* @@ -98,22 +107,30 @@ clusterGroup: golang-external-secrets: name: golang-external-secrets namespace: golang-external-secrets + annotations: + argocd.argoproj.io/sync-wave: '-20' project: hub chart: golang-external-secrets chartVersion: 0.1.* rh-keycloak: name: rh-keycloak namespace: keycloak-system + annotations: + argocd.argoproj.io/sync-wave: '-20' project: hub path: charts/keycloak rh-cert-manager: name: rh-cert-manager namespace: cert-manager-operator + annotations: + argocd.argoproj.io/sync-wave: '-20' project: hub path: charts/certmanager zero-trust-workload-identity-manager: name: zero-trust-workload-identity-manager namespace: zero-trust-workload-identity-manager + annotations: + argocd.argoproj.io/sync-wave: '-10' project: hub path: charts/zero-trust-workload-identity-manager overrides: @@ -122,6 +139,8 @@ clusterGroup: qtodo: name: qtodo namespace: qtodo + annotations: + argocd.argoproj.io/sync-wave: '0' project: hub path: charts/qtodo overrides: