commit 7cdcf51b7fbeb565aad386e9109aa1ae222a8eef Author: Nicolas Massé Date: Thu Dec 14 21:15:02 2023 +0100 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..a259917 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Deploy Red Hat ACS, the GitOps way! + +An all-in-one installation of [Red Hat ACS](https://docs.openshift.com/acs/4.3/welcome/index.html) using GitOps: + +- Red Hat ACS Operator +- **Central** installation +- [Init bundle](https://docs.openshift.com/acs/4.3/installing/installing_ocp/init-bundle-ocp.html) generation +- **Secure Cluster Service** installation +- Dedicated route for the console with the default router certificate. That is to say, no more "invalid certificate" warning! +- Post-configuration hook to deploy Red Hat ACS configuration +- A link to the Central from within the OpenShift Console diff --git a/argocd-app.yaml b/argocd-app.yaml new file mode 100644 index 0000000..646eab3 --- /dev/null +++ b/argocd-app.yaml @@ -0,0 +1,28 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: rhacs + namespace: openshift-gitops +spec: + project: default + source: + repoURL: https://github.com/nmasse-itix/rhacs-gitops.git + targetRevision: main + path: charts/rhacs + helm: + parameters: + - name: masterKey + value: "S3cr3t!" + - name: openshiftDnsZone + value: apps.ocp.tld + destination: + server: https://kubernetes.default.svc + syncPolicy: + syncOptions: + - CreateNamespace=false + automated: + selfHeal: true + prune: true + managedNamespaceMetadata: + labels: + argocd.argoproj.io/managed-by: openshift-gitops diff --git a/charts/rhacs/Chart.yaml b/charts/rhacs/Chart.yaml new file mode 100644 index 0000000..d05de4d --- /dev/null +++ b/charts/rhacs/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: rhacs +type: application +version: 0.0.1 +appVersion: "0.0.1" \ No newline at end of file diff --git a/charts/rhacs/files/stackrox-configure-hook/configure.yaml b/charts/rhacs/files/stackrox-configure-hook/configure.yaml new file mode 100644 index 0000000..dc1f744 --- /dev/null +++ b/charts/rhacs/files/stackrox-configure-hook/configure.yaml @@ -0,0 +1,215 @@ +- name: Configure RHACS + hosts: localhost + gather_facts: no + vars: + ansible_connection: local + acs_api: https://{{ central_hostname }}/v1 + validate_certs: no + tasks: + - name: Get Stackrox central's Route + kubernetes.core.k8s_info: + api_version: route.openshift.io/v1 + kind: Route + name: central + namespace: stackrox + register: central_route + failed_when: central_route.resources|length == 0 + until: central_route is succeeded + retries: 60 + delay: 5 + + - set_fact: + central_hostname: '{{ central_route.resources[0].spec.host }}:443' + + - name: Get Stackrox central's admin password + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: central-admin + namespace: stackrox + register: admin_secret + failed_when: admin_secret.resources|length == 0 + until: admin_secret is succeeded + retries: 60 + delay: 5 + + - set_fact: + central_admin_password: '{{ admin_secret.resources[0].data.password | b64decode }}' + + - name: Check if jmespath is available locally + debug: msg={{ dummy|json_query('@') }} + register: check_jmespath + ignore_errors: yes + vars: + dummy: Hello World + + - name: Ensure JMESPath is installed + assert: + that: + - 'check_jmespath is success' + msg: > + The JMESPath library is required by this playbook. + Please install the JMESPath library with 'pip install jmespath'. + + - name: Wait for the Central to be ready + uri: + url: 'https://{{ central_hostname }}' + validate_certs: '{{ validate_certs }}' + register: healthcheck + changed_when: false + until: healthcheck is succeeded + retries: 60 + delay: 5 + + - name: Get K8s secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: stackrox-cicd-token + namespace: stackrox + register: cicd_token_secret + + - name: Create the CI/CD API Token + uri: + url: '{{ acs_api }}/apitokens/generate' + method: POST + status_code: "200" + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + body: '{{ token_creation }}' + body_format: json + force_basic_auth: yes + register: create_apitoken_response + changed_when: create_apitoken_response.status == 200 + when: cicd_token_secret.resources|length == 0 + vars: + token_creation: + name: tekton-pipelines + role: Continuous Integration + + - set_fact: + apitoken_value: '{{ create_apitoken_response.json.token }}' + when: cicd_token_secret.resources|length == 0 + + - name: Create the K8s Secret + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: stackrox-cicd-token + namespace: stackrox + stringData: + token: '{{ apitoken_value }}' + endpoint: '{{ central_hostname }}' + when: apitoken_value is defined + + - name: Get secrets in the stackrox namespace + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + namespace: stackrox + register: stackrox_secrets + failed_when: stackrox_secrets.resources|length == 0 + + - set_fact: + registry_reader_token: '{{ stackrox_secrets.resources | json_query(query) | first | b64decode }}' + vars: + query: > + [?metadata.annotations."kubernetes.io/service-account.name" == `stackrox-registry-reader` && type == `kubernetes.io/service-account-token`].data.token + + - name: Find image registry integrations + uri: + url: '{{ acs_api }}/imageintegrations' + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + force_basic_auth: yes + register: find_image_integrations_response + changed_when: false + + - set_fact: + image_integration_id: '{{ (find_image_integrations_response.json | json_query(query) | first).id }}' + when: find_image_integrations_response.json | json_query(query) | count > 0 + vars: + query: integrations[?type == `docker` && docker.endpoint == `image-registry.openshift-image-registry.svc:5000`] + + - name: Create the image registry integration + uri: + url: '{{ acs_api }}/imageintegrations' + method: POST + status_code: "200" + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + body: '{{ integration }}' + body_format: json + force_basic_auth: yes + register: create_image_integration_response + changed_when: create_image_integration_response.status == 200 + when: image_integration_id is not defined + vars: + integration: + name: OpenShift Internal Registry + autogenerated: false + categories: + - REGISTRY + clusterId: "" + docker: + endpoint: image-registry.openshift-image-registry.svc:5000 + insecure: true + username: stackrox-registry-reader + password: '{{ registry_reader_token }}' + type: docker + + - set_fact: + image_integration_id: '{{ create_image_integration_response.json.id }}' + when: image_integration_id is not defined + + - debug: + var: image_integration_id + + - name: Find auth providers + uri: + url: '{{ acs_api }}/authProviders' + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + force_basic_auth: yes + register: find_auth_providers_response + changed_when: false + + - set_fact: + auth_provider_id: '{{ (find_auth_providers_response.json | json_query(query) | first).id }}' + when: find_auth_providers_response.json | json_query(query) | count > 0 + vars: + query: authProviders[?name == `OpenShift`] + + - name: Create the OpenShift auth provider + uri: + url: '{{ acs_api }}/authProviders' + method: POST + status_code: "200" + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + body: '{{ auth }}' + body_format: json + force_basic_auth: yes + register: create_auth_provider_response + changed_when: create_auth_provider_response.status == 200 + when: auth_provider_id is not defined + vars: + auth: + name: OpenShift + type: openshift + validated: true + + - set_fact: + auth_provider_id: '{{ create_auth_provider_response.json.id }}' + when: auth_provider_id is not defined + + - debug: + var: auth_provider_id diff --git a/charts/rhacs/files/stackrox-configure-hook/entrypoint.sh b/charts/rhacs/files/stackrox-configure-hook/entrypoint.sh new file mode 100644 index 0000000..19370e9 --- /dev/null +++ b/charts/rhacs/files/stackrox-configure-hook/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -Eeuo pipefail + +ansible-galaxy collection install community.general +ansible-playbook configure.yaml + +exit 0 \ No newline at end of file diff --git a/charts/rhacs/files/stackrox-init-hook/configure-acs.sh b/charts/rhacs/files/stackrox-init-hook/configure-acs.sh new file mode 100644 index 0000000..1521c73 --- /dev/null +++ b/charts/rhacs/files/stackrox-init-hook/configure-acs.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -Eeuo pipefail + +mkdir -p /tmp/bin +curl -sfLo /tmp/bin/roxctl https://mirror.openshift.com/pub/rhacs/assets/4.0.0/bin/Linux/roxctl +chmod 755 /tmp/bin/roxctl +curl -sLo /tmp/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 +chmod 755 /tmp/bin/jq +export PATH="/tmp/bin:$PATH" + +echo "========================================================================" +echo " Connecting to Red Hat ACS" +echo "========================================================================" +echo + +export ROX_CENTRAL_ADDRESS="$(oc get route central -n stackrox -o go-template='{{.spec.host}}'):443" +while ! curl -sfko /dev/null "https://$ROX_CENTRAL_ADDRESS/"; do + echo "Red Hat ACS not ready..." + sleep 5 + + # There is a risk the central's route to be created after this script started + # so we need to periodically refresh it + export ROX_CENTRAL_ADDRESS="$(oc get route central -n stackrox -o go-template='{{.spec.host}}'):443" +done +export ROX_CENTRAL_HOSTNAME="$ROX_CENTRAL_ADDRESS" + +echo "========================================================================" +echo " Retrieving an API Token for Red Hat ACS" +echo "========================================================================" +echo +if ! oc get secret stackrox-api-token -n stackrox &>/dev/null; then + POLICY_JSON='{ "name": "init-token", "role":"Admin"}' + APIURL="https://$ROX_CENTRAL_ADDRESS/v1/apitokens/generate" + export ROX_API_TOKEN=$(curl -s -k -u admin:$ROX_ADMIN_PASSWORD -H 'Content-Type: application/json' -X POST -d "$POLICY_JSON" "$APIURL" | jq -r '.token') + oc create secret generic stackrox-api-token -n stackrox --from-literal=token="$ROX_API_TOKEN" +else + export ROX_API_TOKEN="$(oc get secret stackrox-api-token -n stackrox -o go-template --template='{{.data.token|base64decode}}')" +fi + +echo "========================================================================" +echo " Generating the Cluster Init Bundle" +echo "========================================================================" +echo + +if ! oc get secret admission-control-tls -n stackrox &>/dev/null; then + roxctl -e "$ROX_CENTRAL_ADDRESS" central init-bundles generate local-cluster --output-secrets /tmp/cluster_init_bundle.yaml + oc apply -f /tmp/cluster_init_bundle.yaml -n stackrox +fi + +echo "========================================================================" +echo " Fixing OAuth Authentication" +echo "========================================================================" +echo + +oc annotate -n stackrox serviceaccounts/central serviceaccounts.openshift.io/oauth-redirectreference.alt='{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"central-plain"}}' serviceaccounts.openshift.io/oauth-redirecturi.alt=sso/providers/openshift/callback + +exit 0 \ No newline at end of file diff --git a/charts/rhacs/templates/_helpers.tpl b/charts/rhacs/templates/_helpers.tpl new file mode 100644 index 0000000..abe3ba6 --- /dev/null +++ b/charts/rhacs/templates/_helpers.tpl @@ -0,0 +1,5 @@ +{{/* vim: set filetype=mustache: */}} + +{{- define "acs-admin-password" -}} +{{- trunc 16 (sha256sum (cat .Values.masterKey "acs-admin-password")) -}} +{{- end -}} diff --git a/charts/rhacs/templates/acs.yaml b/charts/rhacs/templates/acs.yaml new file mode 100644 index 0000000..d05ee7c --- /dev/null +++ b/charts/rhacs/templates/acs.yaml @@ -0,0 +1,116 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: + argocd.argoproj.io/sync-wave: "10" + openshift.io/description: "" + openshift.io/display-name: "" + labels: + kubernetes.io/metadata.name: stackrox + name: stackrox +spec: + finalizers: + - kubernetes +--- +apiVersion: v1 +kind: Secret +metadata: + # The secret needs to be created before the creation of the "Central" Custom Resource. + # Otherwise, a race condition is possible and the installation of the Central might get stuck. + annotations: + argocd.argoproj.io/sync-wave: "10" + name: central-admin + namespace: stackrox +type: Opaque +data: + password: {{ include "acs-admin-password" . | b64enc | quote }} +--- +apiVersion: platform.stackrox.io/v1alpha1 +kind: Central +metadata: + annotations: + argocd.argoproj.io/sync-wave: "15" + argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true + name: stackrox-central-services + namespace: stackrox +spec: + central: + exposure: + loadBalancer: + enabled: false + port: 443 + nodePort: + enabled: false + route: + enabled: true + adminPasswordSecret: + name: central-admin + db: + isEnabled: Default + persistence: + persistentVolumeClaim: + claimName: central-db + persistence: + persistentVolumeClaim: + claimName: stackrox-db + egress: + connectivityPolicy: Online + scanner: + analyzer: + scaling: + autoScaling: Disabled + replicas: 3 + scannerComponent: Enabled + # Listen on pain HTTP so that we can expose the central through a Route + customize: + envVars: + - name: ROX_PLAINTEXT_ENDPOINTS + value: http@8080 +--- +apiVersion: platform.stackrox.io/v1alpha1 +kind: SecuredCluster +metadata: + annotations: + argocd.argoproj.io/sync-wave: "30" + argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true + name: stackrox-secured-cluster-services + namespace: stackrox +spec: + auditLogs: + collection: Auto + admissionControl: + listenOnUpdates: true + bypass: BreakGlassAnnotation + contactImageScanners: ScanIfMissing + listenOnCreates: true + timeoutSeconds: 20 + listenOnEvents: true + scanner: + analyzer: + scaling: + autoScaling: Enabled + maxReplicas: 5 + minReplicas: 2 + replicas: 3 + scannerComponent: AutoSense + perNode: + collector: + collection: EBPF + imageFlavor: Regular + taintToleration: TolerateTaints + clusterName: local-cluster +--- +apiVersion: console.openshift.io/v1 +kind: ConsoleLink +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: rhacs-menu +spec: + applicationMenu: + imageURL: >- + /static/assets/redhat.svg + section: RHACS Section + href: 'https://central.{{ .Values.openshiftDnsZone }}/' + location: ApplicationMenu + text: Red Hat Advanced Cluster Security diff --git a/charts/rhacs/templates/hook.yaml b/charts/rhacs/templates/hook.yaml new file mode 100644 index 0000000..71b1eb0 --- /dev/null +++ b/charts/rhacs/templates/hook.yaml @@ -0,0 +1,151 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-hook + namespace: stackrox +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-hook + namespace: stackrox +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: edit +subjects: +- kind: ServiceAccount + name: stackrox-hook + namespace: stackrox +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-hook-scc + namespace: stackrox +rules: +- apiGroups: + - security.openshift.io + resourceNames: + - anyuid + resources: + - securitycontextconstraints + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-hook-scc + namespace: stackrox +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: stackrox-hook-scc +subjects: +- kind: ServiceAccount + name: stackrox-hook + namespace: stackrox +--- +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-init-hook + namespace: stackrox +data: +{{ (.Files.Glob "files/stackrox-init-hook/*").AsConfig | indent 2 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-configure-hook + namespace: stackrox +data: +{{ (.Files.Glob "files/stackrox-configure-hook/*").AsConfig | indent 2 }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-init-hook + namespace: stackrox +spec: + backoffLimit: 30 + template: + spec: + containers: + - name: hook + command: + - /entrypoint/configure-acs.sh + args: [] + image: registry.redhat.io/openshift4/ose-cli:v4.13 + imagePullPolicy: IfNotPresent + env: + - name: ROX_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: central-admin + key: password + - name: USER + value: openshift + - name: HOME + value: /tmp + volumeMounts: + - mountPath: /entrypoint + name: stackrox-hook + readOnly: true + serviceAccountName: stackrox-hook + serviceAccount: stackrox-hook + restartPolicy: OnFailure + terminationGracePeriodSeconds: 30 + volumes: + - name: stackrox-hook + configMap: + name: stackrox-init-hook + defaultMode: 0755 +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-configure-hook + namespace: stackrox +spec: + backoffLimit: 30 + template: + spec: + containers: + - name: hook + command: + - /playbooks/entrypoint.sh + args: [] + image: registry.redhat.io/ansible-automation-platform-21/ee-supported-rhel8:1.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /playbooks + name: stackrox-hook + readOnly: true + workingDir: /playbooks + serviceAccountName: stackrox-hook + serviceAccount: stackrox-hook + restartPolicy: OnFailure + terminationGracePeriodSeconds: 30 + volumes: + - name: stackrox-hook + configMap: + name: stackrox-configure-hook + defaultMode: 0755 diff --git a/charts/rhacs/templates/operator.yaml b/charts/rhacs/templates/operator.yaml new file mode 100644 index 0000000..b2fe3e9 --- /dev/null +++ b/charts/rhacs/templates/operator.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: + argocd.argoproj.io/sync-wave: "0" + openshift.io/description: "" + openshift.io/display-name: "" + labels: + kubernetes.io/metadata.name: rhacs-operator + name: rhacs-operator +spec: + finalizers: + - kubernetes +--- +apiVersion: operators.coreos.com/v1 +kind: OperatorGroup +metadata: + annotations: + argocd.argoproj.io/sync-wave: "0" + name: rhacs-operator + namespace: rhacs-operator +spec: + upgradeStrategy: Default +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + annotations: + argocd.argoproj.io/sync-wave: "10" + name: rhacs-operator + namespace: rhacs-operator +spec: + channel: rhacs-4.2 + installPlanApproval: Automatic + name: rhacs-operator + source: redhat-operators + sourceNamespace: openshift-marketplace diff --git a/charts/rhacs/templates/registry-reader.yaml b/charts/rhacs/templates/registry-reader.yaml new file mode 100644 index 0000000..fdc8c78 --- /dev/null +++ b/charts/rhacs/templates/registry-reader.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-registry-reader + namespace: stackrox +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-registry-reader + namespace: stackrox +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:image-puller +subjects: +- kind: ServiceAccount + name: stackrox-registry-reader + namespace: stackrox +--- diff --git a/charts/rhacs/templates/route.yaml b/charts/rhacs/templates/route.yaml new file mode 100644 index 0000000..12cec33 --- /dev/null +++ b/charts/rhacs/templates/route.yaml @@ -0,0 +1,55 @@ +kind: Service +apiVersion: v1 +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: central-plain + namespace: stackrox +spec: + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP + sessionAffinity: None + selector: + app: central +--- +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: central-plain + namespace: stackrox +spec: + host: central.{{ .Values.openshiftDnsZone }} + to: + kind: Service + name: central-plain + weight: 100 + port: + targetPort: 8080 + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect + wildcardPolicy: None +--- +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: allow-ext-to-central-plain + namespace: stackrox +spec: + podSelector: + matchLabels: + app: central + ingress: + - ports: + - protocol: TCP + port: 8080 + policyTypes: + - Ingress diff --git a/charts/rhacs/values.yaml b/charts/rhacs/values.yaml new file mode 100644 index 0000000..1fec1e9 --- /dev/null +++ b/charts/rhacs/values.yaml @@ -0,0 +1,5 @@ +# DNS Zone delegated to OpenShift (ex: apps.foo.bar) +openshiftDnsZone: "" + +# Master Key used to generate the RHACS admin password +masterKey: "S3cr3t!"