diff --git a/README.md b/README.md index a1ab0d6..3dcad1a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GitOps Artefacts for the MAD Roadshow France 2023 -## Pre-requisites +## Deploy OpenShift resources with OpenShift GitOps * Install the OpenShift GitOps operator. @@ -21,25 +21,46 @@ oc get route -n openshift-gitops openshift-gitops-server -o jsonpath='https://{. * Payload URL: *url above* * Content-Type: Application/json -* Create the required namespaces. +* Give cluster-admin access rights to the **OpenShift Gitops** operator. ```sh -oc new-project fruits-dev +oc adm policy add-cluster-role-to-user cluster-admin system:serviceaccount:openshift-gitops:openshift-gitops-argocd-application-controller ``` -* Label the `fruits-dev` namespace with argocd annotations - ```sh -oc label namespace fruits-dev argocd.argoproj.io/managed-by=openshift-gitops +cp infrastructure.yaml.sample infrastructure.yaml +oc apply -f infrastructure.yaml -n openshift-gitops ``` -* Give admin access rights on the **fruits-dev** namespace to the **OpenShift Gitops** operator. +## Create the Helm repository ```sh -oc adm policy add-role-to-user admin -n fruits-dev system:serviceaccount:openshift-gitops:openshift-gitops-argocd-application-controller +sudo dnf install awscli2 rclone +aws configure +aws s3api list-buckets --output text +aws s3api create-bucket --bucket mad-roadshow-france-2023-helm-charts --create-bucket-configuration LocationConstraint=eu-west-3 --region eu-west-3 +aws s3api put-public-access-block --bucket "mad-roadshow-france-2023-helm-charts" --public-access-block-configuration "BlockPublicPolicy=false" +aws s3api put-bucket-policy --bucket mad-roadshow-france-2023-helm-charts --policy '{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::mad-roadshow-france-2023-helm-charts/*" + + ] + } + ] +}' +rclone config +rclone ls aws:mad-roadshow-france-2023-helm-charts ``` - ## Deploy Postgres CrunchyDB 1. Create a namespace ***preprod-database*** diff --git a/infrastructure/templates/acs.yaml b/infrastructure/templates/acs.yaml index f99141a..e4320bf 100644 --- a/infrastructure/templates/acs.yaml +++ b/infrastructure/templates/acs.yaml @@ -125,12 +125,45 @@ subjects: 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-hook + name: stackrox-init-hook namespace: stackrox data: configure-acs.sh: | @@ -182,6 +215,211 @@ data: exit 0 --- +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-configure-hook + namespace: stackrox +data: + entrypoint.sh: | + #!/bin/bash + + set -Eeuo pipefail + + ansible-galaxy collection install community.general + ansible-playbook configure.yaml + + exit 0 + configure.yaml: | + - 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: Get Cosign public key + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: code-signature + namespace: fruits-dev + register: cosign_secret + failed_when: cosign_secret.resources|length == 0 + until: cosign_secret is succeeded + retries: 60 + delay: 5 + + - set_fact: + cosign_public_key: '{{ cosign_secret.resources[0].data["cosign.pub"] | 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: Find signature integrations + uri: + url: '{{ acs_api }}/signatureintegrations' + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + force_basic_auth: yes + register: find_signature_integrations_response + changed_when: false + + - set_fact: + signature_integration_id: '{{ (find_signature_integrations_response.json | json_query(query) | first).id }}' + when: find_signature_integrations_response.json | json_query(query) | count > 0 + vars: + query: integrations[?name == `Sigstore`] + + - name: Create the Cosign signature integration + uri: + url: '{{ acs_api }}/signatureintegrations' + 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_signature_integration_response + changed_when: create_signature_integration_response.status == 200 + when: signature_integration_id is not defined + vars: + integration: + name: Sigstore + cosign: + publicKeys: + - name: cosign.pub + publicKeyPemEnc: '{{ cosign_public_key }}' + + - set_fact: + signature_integration_id: '{{ create_signature_integration_response.json.id }}' + when: signature_integration_id is not defined + + - debug: + var: signature_integration_id + + - name: Find policies + uri: + url: '{{ acs_api }}/policies?query=Policy%3AImage%20is%20not%20signed' + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + force_basic_auth: yes + register: find_policies_response + changed_when: false + + - set_fact: + policy_id: '{{ (find_policies_response.json.policies | first).id }}' + when: find_policies_response.json.policies | count > 0 + + - name: Create the policy + uri: + url: '{{ acs_api }}/policies' + method: POST + status_code: "200" + validate_certs: '{{ validate_certs }}' + url_username: admin + url_password: '{{ central_admin_password }}' + body: '{{ policy }}' + body_format: json + force_basic_auth: yes + register: create_policy_response + changed_when: create_policy_response.status == 200 + when: policy_id is not defined + vars: + policy: + SORTEnforcement: no + SORTLifecycleStage: '' + SORTName: '' + categories: + - Security Best Practices + criteriaLocked: no + description: The container image has not been digitally signed. + disabled: no + enforcementActions: + - SCALE_TO_ZERO_ENFORCEMENT + - UNSATISFIABLE_NODE_CONSTRAINT_ENFORCEMENT + eventSource: NOT_APPLICABLE + exclusions: [] + isDefault: no + lifecycleStages: + - DEPLOY + mitreAttackVectors: [] + mitreVectorsLocked: no + name: Image is not signed + notifiers: [] + policySections: + - policyGroups: + - booleanOperator: OR + fieldName: Image Signature Verified By + negate: no + values: + - value: "{{ signature_integration_id }}" + sectionName: Policy Section 1 + policyVersion: '1.1' + rationale: The container image MUST be digitally signed in order to prevent tampering. + remediation: Use cosign to sign this image. See https://docs.sigstore.dev/cosign/signing_with_containers/ + scope: + - cluster: + label: + namespace: dev + severity: CRITICAL_SEVERITY + + - set_fact: + policy_id: '{{ create_policy_response.json.id }}' + when: policy_id is not defined + + - debug: + var: policy_id +--- apiVersion: batch/v1 kind: Job metadata: @@ -221,7 +459,7 @@ spec: volumes: - name: stackrox-hook configMap: - name: stackrox-hook + name: stackrox-init-hook defaultMode: 0755 --- apiVersion: platform.stackrox.io/v1alpha1 @@ -256,3 +494,36 @@ spec: imageFlavor: Regular taintToleration: TolerateTaints clusterName: local-cluster +--- +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/infrastructure/templates/fruits-dev.yaml b/infrastructure/templates/fruits-dev.yaml index 91da0e7..1d19aa9 100644 --- a/infrastructure/templates/fruits-dev.yaml +++ b/infrastructure/templates/fruits-dev.yaml @@ -36,6 +36,35 @@ subjects: name: cosign-hook namespace: fruits-dev --- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: secret-reader + namespace: fruits-dev +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] +--- +# The stackrox hook needs to be able to read the cosign public key in order to create the sigstore policy +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + argocd.argoproj.io/sync-wave: "20" + name: stackrox-hook + namespace: fruits-dev +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: secret-reader +subjects: +- kind: ServiceAccount + name: stackrox-hook + namespace: stackrox +--- apiVersion: v1 kind: ConfigMap metadata: @@ -59,7 +88,13 @@ data: echo " Generating a keypair" echo "========================================================================" echo - cosign generate-key-pair --kms k8s://fruits-dev/code-signature + + ## Move to /tmp before creating the keypair because of: + # Error: open cosign.pub: permission denied + # main.go:74: error during command execution: open cosign.pub: permission denied + cd /tmp + + COSIGN_PASSWORD=dummy cosign generate-key-pair k8s://fruits-dev/code-signature fi exit 0