diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8e7438d..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -argocd/tmp-argocd-app-patch.yaml -bootstrap/test.yaml \ No newline at end of file diff --git a/README.md b/README.md index 54f8ce6..b658708 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ DevSecOps CICD pipeline demo using several technologies such as: Vulnerability and configuration management methods included in this demo are the following: -* **Static application security testing (SAST)**, which analyzes code under development for vulnerabilities and quality issues. -* **Software composition analysis (SCA)**, which examines dependent packages included with applications, looking for known vulnerabilities and licensing issues. -* **Interactive application security testing (IAST)** and **dynamic application security testing (DAST)** tools, which analyze running applications to find execution vulnerabilities. -* **Configuration management** with analysis and management of application and infrastructure configurations in DevOps. Traditionally this was not used as a way to improve security. But properly managing configurations in a GitOps process can strengthen security by improving change controls, identifying configuration defects that can reduce the attack surface, and signing and tracking authorship for better accountability and opportunities to improve. -* **Image risk** is any risk associated with a container image. This includes vulnerable dependencies, embedded secrets, bad configurations, malware, or images that are not trusted. +- **Static application security testing (SAST)**, which analyzes code under development for vulnerabilities and quality issues. +- **Software composition analysis (SCA)**, which examines dependent packages included with applications, looking for known vulnerabilities and licensing issues. +- **Interactive application security testing (IAST)** and **dynamic application security testing (DAST)** tools, which analyze running applications to find execution vulnerabilities. +- **Configuration management** with analysis and management of application and infrastructure configurations in DevOps. Traditionally this was not used as a way to improve security. But properly managing configurations in a GitOps process can strengthen security by improving change controls, identifying configuration defects that can reduce the attack surface, and signing and tracking authorship for better accountability and opportunities to improve. +- **Image risk** is any risk associated with a container image. This includes vulnerable dependencies, embedded secrets, bad configurations, malware, or images that are not trusted. This pipeline also improve security adding the following Open Source components: @@ -95,6 +95,38 @@ These policies notification can be enabled by each system policy enabled in our NOTE: By now the integration is manual. WIP to automate it. +## 6. Image Signing and Pipeline Signing + +The original demo can be extended to use Cosign to Sign Image artifacts and also to sign the Tekton Build Pipeline via Tekton [Chaining](https://github.com/tektoncd/chains). + +To extend the pipeline run the extend.sh script + +```sh + ./extend.sh +``` + +This will install Noobaa(Object Storage), Quay, and create a pod for cosign secret generation and verification.It will also install the tekton chains operator and integrate with ACS policies to generate violations for non signed images. + +After installation the pipeline will build images to quay and have a task that signs the image. + + +We also create a policy in ACS that will generate a violation for every unsigned image + + +Pipeline can be run normally via the Run the demo Instructions below. + +After Pipeline is run Quay will show the image signed by Cosign + + +Since we have Tekton Chaining enabled, successfully completed Taskruns will also be annotated with cosign signatures and payload information. + + +And we can verify the signature and payload information of our last successful pipelinerun using the below command. + +```sh + ./demo.sh sign-verify +``` + ## Security Policies and CI Violations In this demo, we can control the security policies applied into our pipelines, scanning the images and analysing the different deployments templates used for deploy our applications. @@ -114,9 +146,10 @@ This ensures that we have the total control of our pipelines, and no image is pu To show a complete demo and show the transition from a "bad image" to an image that passes the build enforcement, we can update the Tekton task of the image build and fix the image. In this example, we will be enabling the enforcement of the "Red Hat Package Manager in Image" policy in ACS, which will fail our pipeline at the image-check as both `yum` and `rpm` package managers are present in our base image. Update the tekton task: + 1. Delete the `s2i-java-11` task - 1. With the UI: From the OpenShift UI, make sure you are in the cicd project and then go to Pipelines > Tasks and delete the `s2i-java-11` task. - 2. With the Tekton cli `tkn task delete s2i-java-11` + 1. With the UI: From the OpenShift UI, make sure you are in the cicd project and then go to Pipelines > Tasks and delete the `s2i-java-11` task. + 2. With the Tekton cli `tkn task delete s2i-java-11` 2. Apply the new update task: `kubectl apply -f fix-image/s2ijava-mgr.yaml` 3. Re-run the pipeline, your deployment now succeeds. @@ -188,20 +221,20 @@ NOTE: This pipeline will fail if you don't [disable the "Fixable at least Import ## Quick Video with the Demo -* [Option I - Complete CICD End2End process (Success)](https://youtu.be/uA7nUYchY5Q) +- [Option I - Complete CICD End2End process (Success)](https://youtu.be/uA7nUYchY5Q) -* [Option II - Failure CICD pipeline due to the ACS violation policy](https://youtu.be/jTRImofd6wQ?t=380) +- [Option II - Failure CICD pipeline due to the ACS violation policy](https://youtu.be/jTRImofd6wQ?t=380) -* [Openshift Coffee Break - ACS for Kubernetes - DevSecOps Way](https://youtu.be/43Mr30mXq0I?t=1955) +- [Openshift Coffee Break - ACS for Kubernetes - DevSecOps Way](https://youtu.be/43Mr30mXq0I?t=1955) ## Promote Pipeline and Triggers -* [Promote Pipeline](docs/promote.md) -* [Triggers in Dev Pipeline](doc/triggers.md) +- [Promote Pipeline](docs/promote.md) +- [Triggers in Dev Pipeline](doc/triggers.md) # Troubleshooting -* [Check the Tshoot section](docs/tshoot.md) +- [Check the Tshoot section](docs/tshoot.md) # Credits diff --git a/bootstrap/deploy_demo.yaml b/bootstrap/deploy_demo.yaml index e273e30..4db8b01 100644 --- a/bootstrap/deploy_demo.yaml +++ b/bootstrap/deploy_demo.yaml @@ -28,3 +28,4 @@ - name: 'Install the ACS Post Content' include_role: name: "ocp4-post-acs" + \ No newline at end of file diff --git a/bootstrap/deploy_signing.yaml b/bootstrap/deploy_signing.yaml new file mode 100644 index 0000000..86e745e --- /dev/null +++ b/bootstrap/deploy_signing.yaml @@ -0,0 +1,18 @@ +--- +- name: 'Extend Original Demo for Image and TaskRun Signing' + hosts: localhost + connection: local + tasks: + - name: 'Install NooBaa' + include_role: + name: "ocp4-install-noobaa" + + - name: 'Install and configure Quay' + include_role: + name: "ocp4-install-quay" + + - name: 'Install and Enable the infra for Signing and Tekton Chaining' + include_role: + name: "ocp4-install-signing" + + \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/defaults/main.yaml b/bootstrap/roles/ocp4-install-noobaa/defaults/main.yaml new file mode 100644 index 0000000..5fe894a --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/defaults/main.yaml @@ -0,0 +1,3 @@ +noobaa_storage_class: "" +noobaa_size: "50Gi" +backing_store_name: "noobaa-pv-backing-store" diff --git a/bootstrap/roles/ocp4-install-noobaa/tasks/main.yaml b/bootstrap/roles/ocp4-install-noobaa/tasks/main.yaml new file mode 100644 index 0000000..5954702 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/tasks/main.yaml @@ -0,0 +1,2 @@ + +- import_tasks: noobaa-create.yaml \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/tasks/noobaa-create.yaml b/bootstrap/roles/ocp4-install-noobaa/tasks/noobaa-create.yaml new file mode 100644 index 0000000..91868ae --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/tasks/noobaa-create.yaml @@ -0,0 +1,125 @@ +- name: Get cluster version + k8s_info: + api_version: config.openshift.io/v1 + kind: ClusterVersion + name: version + register: r_cluster_version + +- name: Set ocp4_cluster_version fact + set_fact: + ocp4_cluster_version: "{{ r_cluster_version.resources[0].status.history[0].version }}" + +- name: Obtain Channel from Version + set_fact: + ocp4_channel: "{{ ocp4_cluster_version.split('.') }}" + +- name: Set Openshift Channel + set_fact: + ocp4_channel: "stable-{{ ocp4_channel[0] + '.' + ocp4_channel[1] }}" + +- name: Print OpenShift version + debug: + msg: "{{ ocp4_channel }}" + +- name: Adapt to the openshift_cluster_version LESS than 4.9 + when: ocp4_cluster_version is version_compare('4.9', '<') + block: + - name: Create OpenShift Objects to install Noobaa + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/odf-namespace.yaml.j2 + - ./templates/operatorgroup-storage.yaml.j2 + - ./templates/ocs-subscription.yaml.j2 + + - name: Wait for NooBaa CRD to exist + kubernetes.core.k8s_info: + api_version: "apiextensions.k8s.io/v1beta1" + kind: CustomResourceDefinition + name: "noobaas.noobaa.io" + register: crds + until: crds.resources|length > 0 + retries: 30 + delay: 10 + +- name: Adapt to the openshift_cluster_version MORE than 4.9 + when: ocp4_cluster_version is version_compare('4.9', '>=') + block: + - name: Create OpenShift Objects to install Noobaa + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/odf-namespace.yaml.j2 + - ./templates/operatorgroup-storage.yaml.j2 + - ./templates/odf-subscription.yaml.j2 + + - name: Wait for NooBaa CRD to exist + kubernetes.core.k8s_info: + api_version: "apiextensions.k8s.io/v1" + kind: CustomResourceDefinition + name: "noobaas.noobaa.io" + register: crds + until: crds.resources|length > 0 + retries: 30 + delay: 10 + +- name: Create Noobaa Object + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/noobaa-object.yaml.j2 + +- name: Wait Until NooBaa Object is Ready + shell: | + oc get noobaas.noobaa.io/noobaa -n openshift-storage -o jsonpath='{.status.phase}' + register: noobaa_status + retries: 10 + delay: 20 + until: + - noobaa_status.stdout == "Ready" + +- name: Get Default Openshift Storage Class + shell: | + oc get sc -o=jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}' + register: default_openshift_storage_class + when: noobaa_storage_class == "" + +- name: Get any other Storage Class + shell: | + oc get sc -o name | head -n 1 | cut -d "/" -f2 + register: other_openshift_storage_class + when: (default_openshift_storage_class.stdout |default("") == "" ) and (noobaa_storage_class == "") + +- name: Use default storage class if it was set + ansible.builtin.set_fact: + noobaa_storage_class: "{{ default_openshift_storage_class.stdout }}" + when: (default_openshift_storage_class.stdout |default("") != "" ) and (noobaa_storage_class == "") + +- name: Try other possible storage class if no defined/default storage class + ansible.builtin.set_fact: + noobaa_storage_class: "{{ other_openshift_storage_class.stdout }}" + when: (default_openshift_storage_class.stdout |default("") == "" ) and (noobaa_storage_class == "") and (other_openshift_storage_class|default("") != "") + +- name: Create NooBaa Backing Store + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/noobaa-backingstore.yaml.j2 + +- name: Wait Until NooBaa Object is Ready + shell: | + oc get BackingStore/"{{ backing_store_name }}" -n openshift-storage -o jsonpath='{.status.phase}' + register: backing_store + retries: 10 + delay: 20 + until: + - backing_store.stdout == "Ready" + +- name: Patch Bucket Class with Backing Store + shell: | + oc patch bucketclass noobaa-default-bucket-class --patch '{"spec":{"placementPolicy":{"tiers":[{"backingStores":["{{backing_store_name}}"]}]}}}' --type merge -n openshift-storage + \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-backingstore.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-backingstore.yaml.j2 new file mode 100644 index 0000000..227c2c5 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-backingstore.yaml.j2 @@ -0,0 +1,17 @@ +apiVersion: noobaa.io/v1alpha1 +kind: BackingStore +metadata: + finalizers: + - noobaa.io/finalizer + labels: + app: noobaa + name: noobaa-pv-backing-store + namespace: openshift-storage +spec: + pvPool: + numVolumes: 1 + resources: + requests: + storage: {{ noobaa_size }} + storageClass: {{ noobaa_storage_class }} + type: pv-pool \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-object.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-object.yaml.j2 new file mode 100644 index 0000000..690de16 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/noobaa-object.yaml.j2 @@ -0,0 +1,15 @@ +apiVersion: noobaa.io/v1alpha1 +kind: NooBaa +metadata: + name: noobaa + namespace: openshift-storage +spec: + dbResources: + requests: + cpu: '0.1' + memory: 1Gi + dbType: postgres + coreResources: + requests: + cpu: '0.1' + memory: 1Gi \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/ocs-subscription.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/ocs-subscription.yaml.j2 new file mode 100644 index 0000000..df02380 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/ocs-subscription.yaml.j2 @@ -0,0 +1,11 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: ocs-operator + namespace: openshift-storage +spec: + channel: {{ ocp4_channel }} + installPlanApproval: Automatic + name: ocs-operator + source: redhat-operators + sourceNamespace: openshift-marketplace \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/odf-namespace.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/odf-namespace.yaml.j2 new file mode 100644 index 0000000..ef83eda --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/odf-namespace.yaml.j2 @@ -0,0 +1,8 @@ +kind: Namespace +apiVersion: v1 +metadata: + name: openshift-storage + labels: + kubernetes.io/metadata.name: openshift-storage +spec: {} + diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/odf-subscription.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/odf-subscription.yaml.j2 new file mode 100644 index 0000000..0a4c464 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/odf-subscription.yaml.j2 @@ -0,0 +1,13 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + labels: + operators.coreos.com/odf-operator.openshift-storage: '' + name: odf-operator + namespace: openshift-storage +spec: + channel: {{ ocp4_channel }} + installPlanApproval: Automatic + name: odf-operator + source: redhat-operators + sourceNamespace: openshift-marketplace diff --git a/bootstrap/roles/ocp4-install-noobaa/templates/operatorgroup-storage.yaml.j2 b/bootstrap/roles/ocp4-install-noobaa/templates/operatorgroup-storage.yaml.j2 new file mode 100644 index 0000000..59e2bf2 --- /dev/null +++ b/bootstrap/roles/ocp4-install-noobaa/templates/operatorgroup-storage.yaml.j2 @@ -0,0 +1,8 @@ +apiVersion: operators.coreos.com/v1 +kind: OperatorGroup +metadata: + name: openshift-storage-test + namespace: openshift-storage +spec: + targetNamespaces: + - openshift-storage \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-quay/defaults/main.yaml b/bootstrap/roles/ocp4-install-quay/defaults/main.yaml new file mode 100644 index 0000000..f845d2e --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/defaults/main.yaml @@ -0,0 +1,29 @@ +quay_project_name: quay-demo +quay_admin_username: quayadmin +quay_admin_email: quayadmin@redhat.com +quay_admin_password: quaypass123 +quay_registry_name: demo-registry +quay_org_name: cicd-demo +quay_secret_name: quay-robot-secret +quay_repositories: + - spring-petclinic-dev + - spring-petclinic-stage +quay_robot_account: demo_robot +pipeline_namespace: cicd +csrf_pattern: ".*window.__token\ =\ '(.*)';.*" +#Can obtain status codes from Swagger of quay route/api/v1/discovery +quay_user_found_success_status_code: 200 +quay_org_not_found_error_code: 404 +quay_org_found_success_status_code: 200 +quay_org_created_success_status_code: 201 +quay_repo_not_found_error_code: 404 +quay_repo_found_success_status_code: 200 +quay_repo_created_success_status_code: 201 +quay_robot_not_found_error_code: 400 +quay_robot_found_success_status_code: 200 +quay_robot_created_success_status_code: 201 +quay_perm_success_status_code: 200 +secret_required_namespaces: + - cicd + - devsecops-dev + - devsecops-qa \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-quay/tasks/configure-quay.yaml b/bootstrap/roles/ocp4-install-quay/tasks/configure-quay.yaml new file mode 100644 index 0000000..5aba2b6 --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/tasks/configure-quay.yaml @@ -0,0 +1,421 @@ +# Code attribution +# Some code from here - https://github.com/redhat-cop/rhel-edge-automation-arch/blob/main/ansible/roles/quay-raw-create-user/tasks/main.yaml + +- name: extract quay hostname + shell: | + oc get route {{ quay_registry_name }}-quay -o jsonpath='{.spec.host}' -n {{ quay_project_name }} + register: quay_hostname_result + delay: 5 + retries: 10 + until: + - quay_hostname_result.stdout != "" + +- name: Set Quay hostname + ansible.builtin.set_fact: + quay_route: "{{ quay_hostname_result.stdout }}" + +- name: Wait until Quay Application is Responding + ansible.builtin.uri: + return_content: yes + status_code: 200 + url: "https://{{ quay_route }}" + validate_certs: no + delay: 10 + register: quay_check + retries: 30 + until: + quay_check.status == 200 + +- name: Initialize Quay User + ansible.builtin.uri: + body_format: json + body: + username: "{{ quay_admin_username }}" + email: "{{ quay_admin_email }}" + password: "{{ quay_admin_password }}" + access_token: "true" + headers: + Content-Type: application/json + method: POST + url: "https://{{ quay_route }}/api/v1/user/initialize" + validate_certs: no + register: quay_init_response + failed_when: + - quay_init_response.status != 200 + ignore_errors: true + +- name: Set Output Message from Quay on User Initalize + ansible.builtin.set_fact: + init_output_msg: "{{ quay_init_response.json.message|default('') }}" + ignore_errors: true + +- name: Use API Token to continue Creating + block: + + - name: Set Quay Access Token + ansible.builtin.set_fact: + quay_access_token: "{{ quay_init_response.json.access_token }}" + + - name: Check if Quay Organization Exists + ansible.builtin.uri: + body_format: json + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: GET + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}" + validate_certs: no + register: quay_org_check + failed_when: > + (quay_org_check.status != quay_org_not_found_error_code) and + (quay_org_check.status != quay_org_found_success_status_code) + + - name: Create Quay Organization + ansible.builtin.uri: + body_format: json + body: + name: "{{ quay_org_name }}" + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: POST + url: "https://{{ quay_route }}/api/v1/organization/" + validate_certs: no + register: quay_org_response + failed_when: + - quay_org_response.status != quay_org_created_success_status_code + when: quay_org_check.status|default("") == quay_org_not_found_error_code + + - name: Check if Repository Already Exists + ansible.builtin.uri: + body_format: json + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: GET + url: "https://{{ quay_route }}/api/v1/repository/{{ quay_org_name }}/{{ item }}" + validate_certs: no + register: quay_repo_check + failed_when: + (quay_repo_check.status != quay_repo_not_found_error_code) and + (quay_repo_check.status != quay_repo_found_success_status_code) + + - name: Create Repository + ansible.builtin.uri: + body_format: json + body: + repository: "{{ item }}" + namespace: "{{ quay_org_name }}" + repo_kind: "image" + visibility: "public" + description: "Repo for CICD Demo" + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: POST + url: "https://{{ quay_route }}/api/v1/repository" + validate_certs: no + register: quay_repo_response + failed_when: + - quay_repo_response.status != quay_repo_created_success_status_code + when: quay_repo_check.status|default("") == quay_repo_not_found_error_code + + - name: Check if Robot Account Already Exists + ansible.builtin.uri: + body_format: json + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: GET + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}/robots/{{ quay_robot_account }}" + validate_certs: no + register: quay_robot_check + failed_when: > + (quay_robot_check.status != quay_robot_not_found_error_code) and + (quay_robot_check.status != quay_robot_found_success_status_code) + + - name: Set Robot Token from Check Response + ansible.builtin.set_fact: + quay_robot_token: "{{ quay_robot_check.json.token }}" + when: quay_robot_check.status|default("") == quay_robot_found_success_status_code + + - name: Create Robot Account + ansible.builtin.uri: + body_format: json + body: + description": "Robot Account for CICD Demo" + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: PUT + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}/robots/{{ quay_robot_account }}" + validate_certs: no + register: quay_robot_response + failed_when: + - quay_robot_response.status != quay_robot_created_success_status_code + when: quay_robot_check.status == quay_robot_not_found_error_code + + - name: Set Robot Token from Creating New Robot Account + ansible.builtin.set_fact: + quay_robot_token: "{{ quay_robot_response.json.token }}" + when: quay_robot_response.status|default("") == quay_robot_created_success_status_code + + - name: Add Robot account permissions to repo + ansible.builtin.uri: + body_format: json + body: + role: admin + headers: + Content-Type: application/json + Authorization: "Bearer {{ quay_access_token }}" + method: PUT + url: "https://{{ quay_route }}/api/v1/repository/{{ quay_org_name }}/{{ item }}/permissions/user/{{ quay_org_name }}+{{ quay_robot_account }}" + validate_certs: no + register: quay_perm_response + failed_when: + - quay_perm_response.status != quay_perm_success_status_code + when: + - "'Cannot initialize user in a non-empty database' not in init_output_msg" + +- name: Use Manual Login to try get token + block: + - name: Wait for Quay Application and Load Initial CSRF Token + ansible.builtin.uri: + return_content: yes + status_code: 200 + url: "https://{{ quay_route }}" + validate_certs: no + delay: 10 + register: quay_app_response + retries: 2 + + - name: Set CSRF Token + ansible.builtin.set_fact: + x_csrf_token: "{{ quay_app_response.content | regex_search(csrf_pattern, '\\1', multiline=True) | first }}" + + - name: Try and Create a new session + ansible.builtin.uri: + body_format: json + body: + username: "{{ quay_admin_username }}" + email: "{{ quay_admin_email }}" + password: "{{ quay_admin_password }}" + headers: + cookie: "{{ quay_app_response.cookies_string }}" + x-csrf-token: "{{ x_csrf_token | urlencode }}" + Content-Type: application/json + method: POST + url: "https://{{ quay_route }}/api/v1/signin" + validate_certs: no + follow_redirects: all + register: quay_session_response + failed_when: + - quay_session_response.status != 200 + - quay_session_response.json.success != "true" + + - name: Get Next CSRF Token + ansible.builtin.set_fact: + x_next_csrf_token: "{{ quay_session_response.x_next_csrf_token }}" + + - name: Get user Information + ansible.builtin.uri: + body_format: json + headers: + cookie: "{{ quay_app_response.cookies_string }}" + x-csrf-token: "{{ x_csrf_token | urlencode }}" + Content-Type: application/json + method: GET + url: "https://{{ quay_route }}/api/v1/users/{{ quay_admin_username }}" + validate_certs: no + follow_redirects: all + register: quay_user_response + failed_when: + - quay_session_response.status != quay_user_found_success_status_code + + - name: Set Present Cookie String from User Info Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_session_response.cookies_string }}" + + - name: Check if Quay Organization Exists + ansible.builtin.uri: + body_format: json + headers: + cookie: "{{ present_cookie_string }}" + Content-Type: application/json + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: GET + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}" + validate_certs: no + register: quay_org_check + failed_when: > + (quay_org_check.status != quay_org_not_found_error_code) and + (quay_org_check.status != quay_org_found_success_status_code) + + - name: Set Present Cookie String from Org Check Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_org_check.cookies_string }}" + when: quay_org_check.status|default("") == quay_org_found_success_status_code + + - name: Create Quay Organization + ansible.builtin.uri: + body_format: json + body: + name: "{{ quay_org_name }}" + headers: + cookie: "{{ quay_session_response.cookies_string }}" + Content-Type: application/json + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: POST + url: "https://{{ quay_route }}/api/v1/organization/" + validate_certs: no + register: quay_org_response + failed_when: + - quay_org_response.status != quay_org_created_success_status_code + when: quay_org_check.status|default("") == quay_org_not_found_error_code + + - name: Set Present Cookie String from Org Creation Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_org_response.cookies_string }}" + when: quay_org_response.status|default("") == quay_org_created_success_status_code + + - name: Check if Repository Already Exists + ansible.builtin.uri: + body_format: json + headers: + cookie: "{{ present_cookie_string }}" + Content-Type: application/json + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: GET + url: "https://{{ quay_route }}/api/v1/repository/{{ quay_org_name }}/{{ item }}" + validate_certs: no + register: quay_repo_check + failed_when: + (quay_repo_check.status != quay_repo_not_found_error_code) and + (quay_repo_check.status != quay_repo_found_success_status_code) + + - name: Set Cookie String From Repo Check Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_repo_check.cookies_string }}" + when: quay_repo_check.status|default("") == quay_repo_found_success_status_code + + - name: Create Repository + ansible.builtin.uri: + body_format: json + body: + repository: "{{ item }}" + namespace: "{{ quay_org_name }}" + repo_kind: "image" + visibility: "public" + description: "Repo for CICD Demo" + headers: + Content-Type: application/json + cookie: "{{ present_cookie_string }}" + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: POST + url: "https://{{ quay_route }}/api/v1/repository" + validate_certs: no + register: quay_repo_response + failed_when: + - quay_repo_response.status != quay_repo_created_success_status_code + when: quay_repo_check.status|default("") == quay_repo_not_found_error_code + + - name: Set Cookie String From Repo Creation Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_repo_response.cookies_string }}" + when: quay_repo_response.status|default("") == quay_repo_created_success_status_code + + - name: Check if Robot Account Already Exists + ansible.builtin.uri: + body_format: json + headers: + cookie: "{{ present_cookie_string }}" + Content-Type: application/json + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: GET + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}/robots/{{ quay_robot_account }}" + validate_certs: no + register: quay_robot_check + failed_when: > + (quay_robot_check.status != quay_robot_not_found_error_code) and + (quay_robot_check.status != quay_robot_found_success_status_code) + + - name: Set Robot Token from Check Response + ansible.builtin.set_fact: + quay_robot_token: "{{ quay_robot_check.json.token }}" + when: quay_robot_check.status|default("") == quay_robot_found_success_status_code + + - name: Set Cookie String From Robot Check Response + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_robot_check.cookies_string }}" + when: quay_robot_check.status|default("") == quay_robot_found_success_status_code + + - name: Create Robot Account + ansible.builtin.uri: + body_format: json + body: + description": "Robot Account for CICD Demo" + headers: + Content-Type: application/json + cookie: "{{ present_cookie_string }}" + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: PUT + url: "https://{{ quay_route }}/api/v1/organization/{{ quay_org_name }}/robots/{{ quay_robot_account }}" + validate_certs: no + register: quay_robot_response + failed_when: + - quay_robot_response.status != quay_robot_created_success_status_code + when: quay_robot_check.status == quay_robot_not_found_error_code + + - name: Set Robot Token from Creating New Robot Account + ansible.builtin.set_fact: + quay_robot_token: "{{ quay_robot_response.json.token }}" + when: quay_robot_response.status|default("") == quay_robot_created_success_status_code + + - name: Set Cookie String From Robot Creation + ansible.builtin.set_fact: + present_cookie_string: "{{ quay_robot_response.cookies_string }}" + when: quay_robot_response.status|default("") == quay_robot_created_success_status_code + + - name: Add Robot account permissions to repo + ansible.builtin.uri: + body_format: json + body: + role: admin + headers: + Content-Type: application/json + cookie: "{{ present_cookie_string }}" + x-csrf-token: "{{ x_next_csrf_token | urlencode }}" + method: PUT + url: "https://{{ quay_route }}/api/v1/repository/{{ quay_org_name }}/{{ item }}/permissions/user/{{ quay_org_name }}+{{ quay_robot_account }}" + validate_certs: no + register: quay_perm_response + failed_when: + - quay_perm_response.status != quay_perm_success_status_code + when: + - "'Cannot initialize user in a non-empty database' in init_output_msg" + +- name: Delete any Previously Existing Quay Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + namespace: "{{ item }}" + name: "{{ quay_secret_name }}" + wait: yes + loop: "{{ secret_required_namespaces }}" + +#Create Quay Secret so other roles can use +- name: Create Quay Secret in Namespaces that require secret + shell: | + oc create secret docker-registry "{{ quay_secret_name }}" --docker-server="{{ quay_route }}" --docker-username="{{ quay_org_name }}+{{ quay_robot_account }}" --docker-password="{{ quay_robot_token }}" --docker-email="" -n "{{ item }}" + loop: "{{ secret_required_namespaces }}" + +- name: Confirm Quay Secret is Created + kubernetes.core.k8s: + state: present + api_version: v1 + kind: Secret + namespace: "{{ item }}" + name: "{{ quay_secret_name }}" + wait: yes + loop: "{{ secret_required_namespaces }}" \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-quay/tasks/install-quay.yaml b/bootstrap/roles/ocp4-install-quay/tasks/install-quay.yaml new file mode 100644 index 0000000..eb9342d --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/tasks/install-quay.yaml @@ -0,0 +1,81 @@ +- name: Create OpenShift Objects to install Quay + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/quay-subscription.yaml.j2 + - ./templates/quay-namespace.yaml.j2 + +#Only necessary if Quay not managing storage +# - name: Get NooBaa Secret +# shell: | +# oc get noobaa/noobaa -n openshift-storage -o json | jq -r '.status.accounts.admin.secretRef.name' +# register: noobaa_secret + +# - name: Set NooBaa Secret Value +# ansible.builtin.set_fact: +# NOOBAA_SECRET: "{{ noobaa_secret.stdout }}" + +# - name: Get NooBaa Mgmt Address +# shell: | +# oc get noobaa noobaa -n openshift-storage -o json | jq -r '.status.services.serviceMgmt.nodePorts[0]' +# register: noobaa_mgmt + +# - name: Set NooBaa Mgmt Address +# ansible.builtin.set_fact: +# NOOBAA_MGMT: "{{ noobaa_mgmt.stdout }}" + +# - name: Get NooBaa S3 Address +# shell: | +# oc get noobaa noobaa -n openshift-storage -o json | jq -r '.status.services.serviceS3.nodePorts[0]' +# register: noobaa_s3 + +# - name: Set NooBaa S3 Address +# ansible.builtin.set_fact: +# NOOBAA_S3: "{{ noobaa_s3.stdout }}" + +# - name: Get NooBaa Access Key +# shell: | +# oc get secret "{{ NOOBAA_SECRET }}" -n openshift-storage -o json | jq -r '.data.AWS_ACCESS_KEY_ID|@base64d' +# register: noobaa_access_key + +# - name: Set NooBaa Access Key +# ansible.builtin.set_fact: +# NOOBAA_ACCESS_KEY: "{{ noobaa_access_key.stdout }}" + +# - name: Get NooBaa Secret Key +# shell: | +# oc get secret $NOOBAA_SECRET -n openshift-storage -o json | jq -r '.data.AWS_SECRET_ACCESS_KEY|@base64d' +# register: noobaa_secret_key + +# - name: Set NooBaa Secret Key +# ansible.builtin.set_fact: +# NOOBAA_SECRET_KEY: "{{ noobaa_secret_key.stdout }}" + +- name: Wait for QuayRegistry CRD to exist + kubernetes.core.k8s_info: + api_version: "apiextensions.k8s.io/v1" + kind: CustomResourceDefinition + name: "quayregistries.quay.redhat.com" + register: crds + until: crds.resources|length > 0 + retries: 30 + delay: 10 + +- name: Create Quay Registry Object + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/quay-config-secret.yaml.j2 + - ./templates/quayregistry.yaml.j2 + + + + + + + + + + diff --git a/bootstrap/roles/ocp4-install-quay/tasks/main.yaml b/bootstrap/roles/ocp4-install-quay/tasks/main.yaml new file mode 100644 index 0000000..c8d8d4b --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/tasks/main.yaml @@ -0,0 +1,4 @@ +- import_tasks: install-quay.yaml + +- include_tasks: configure-quay.yaml + loop: "{{ quay_repositories }}" diff --git a/bootstrap/roles/ocp4-install-quay/templates/quay-config-secret.yaml.j2 b/bootstrap/roles/ocp4-install-quay/templates/quay-config-secret.yaml.j2 new file mode 100644 index 0000000..35441f4 --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/templates/quay-config-secret.yaml.j2 @@ -0,0 +1,17 @@ +apiVersion: v1 +stringData: + config.yaml: | + FEATURE_GENERAL_OCI_SUPPORT: true + FEATURE_USER_INITIALIZE: true + BROWSER_API_CALLS_XHR_ONLY: false + SUPER_USERS: + - {{ quay_admin_username }} + FEATURE_USER_CREATION: false +kind: Secret +metadata: + name: quay-config-bundle + namespace: {{ quay_project_name }} +type: Opaque + + + diff --git a/bootstrap/roles/ocp4-install-quay/templates/quay-namespace.yaml.j2 b/bootstrap/roles/ocp4-install-quay/templates/quay-namespace.yaml.j2 new file mode 100644 index 0000000..6bc0e8a --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/templates/quay-namespace.yaml.j2 @@ -0,0 +1,8 @@ +kind: Namespace +apiVersion: v1 +metadata: + name: {{ quay_project_name }} + labels: + kubernetes.io/metadata.name: {{ quay_project_name }} +spec: {} + diff --git a/bootstrap/roles/ocp4-install-quay/templates/quay-subscription.yaml.j2 b/bootstrap/roles/ocp4-install-quay/templates/quay-subscription.yaml.j2 new file mode 100644 index 0000000..bd1eff9 --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/templates/quay-subscription.yaml.j2 @@ -0,0 +1,11 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: quay-operator + namespace: openshift-operators +spec: + channel: stable-3.7 + installPlanApproval: Automatic + name: quay-operator + source: redhat-operators + sourceNamespace: openshift-marketplace diff --git a/bootstrap/roles/ocp4-install-quay/templates/quayregistry.yaml.j2 b/bootstrap/roles/ocp4-install-quay/templates/quayregistry.yaml.j2 new file mode 100644 index 0000000..0c74740 --- /dev/null +++ b/bootstrap/roles/ocp4-install-quay/templates/quayregistry.yaml.j2 @@ -0,0 +1,30 @@ +apiVersion: quay.redhat.com/v1 +kind: QuayRegistry +metadata: + name: {{ quay_registry_name }} + namespace: {{ quay_project_name }} +spec: + configBundleSecret: quay-config-bundle + components: + - managed: true + kind: clair + - managed: true + kind: postgres + - managed: true + kind: objectstorage + - managed: true + kind: redis + - managed: true + kind: horizontalpodautoscaler + - managed: true + kind: route + - managed: true + kind: mirror + - managed: true + kind: monitoring + - managed: true + kind: tls + - managed: true + kind: quay + - managed: true + kind: clairpostgres diff --git a/bootstrap/roles/ocp4-install-signing/defaults/main.yaml b/bootstrap/roles/ocp4-install-signing/defaults/main.yaml new file mode 100644 index 0000000..d1cd9f0 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/defaults/main.yaml @@ -0,0 +1,28 @@ +tekton_chain_keys: + "artifacts.oci.storage": "" + "artifacts.taskrun.format": "tekton" + "artifacts.taskrun.storage": "tekton" + "artifacts.oci.signer": "x509" + "artifacts.oci.format": "simplesigning" +#tekton_operator_namespace: openshift-pipelines +pipeline_namespace: cicd +dev_namespace: devsecops-dev +qa_namespace: devsecops-qa +tekton_operator_namespace: tekton-chains +tekton_chain_version: 'v0.10.0' +tekton_install_type: manifest +cosign_image: "image-registry.openshift-image-registry.svc:5000/{{ pipeline_namespace }}/cosign-pod:latest" +#cosign_image: "gcr.io/projectsigstore/cosign:v1.9.0" +secret_generate_name: signing-secrets +stackrox_central_admin_password: stackrox +quay_secret_name: quay-robot-secret +quay_registry_name: demo-registry +quay_org_name: cicd-demo +quay_project_name: quay-demo +quay_repositories: + - spring-petclinic-dev + - spring-petclinic-stage +secret_required_namespaces: + - cicd + - devsecops-dev + - devsecops-qa \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/files/policies/signed-image-policy.json b/bootstrap/roles/ocp4-install-signing/files/policies/signed-image-policy.json new file mode 100644 index 0000000..7764131 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/files/policies/signed-image-policy.json @@ -0,0 +1,47 @@ +{ + "policies": [ + { + "id": "c8fde2c3-980c-40e3-bc9d-6245b13ab81e", + "name": "Trusted_Signature_Image_Policy", + "description": "Alert on Images that have not been signed", + "rationale": "rationale", + "remediation": "All images should be signed by our cosign-demo signature", + "disabled": false, + "categories": [ + "Security Best Practices" + ], + "lifecycleStages": [ + "BUILD", + "DEPLOY" + ], + "severity": "HIGH_SEVERITY", + "enforcementActions": [], + "notifiers": [], + "SORTName": "", + "SORTLifecycleStage": "", + "SORTEnforcement": false, + "policyVersion": "1.1", + "policySections": [ + { + "sectionName": "Policy Section 1", + "policyGroups": [ + { + "fieldName": "Image Signature Verified By", + "booleanOperator": "OR", + "negate": false, + "values": [ + { + "value": "io.stackrox.signatureintegration.f9352803-d5c9-45d6-abe0-e1361a24559a" + } + ] + } + ] + } + ], + "mitreAttackVectors": [], + "criteriaLocked": false, + "mitreVectorsLocked": false, + "isDefault": false + } + ] +} \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/tasks/add-cosign-secret-acs.yaml b/bootstrap/roles/ocp4-install-signing/tasks/add-cosign-secret-acs.yaml new file mode 100644 index 0000000..89904ce --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/tasks/add-cosign-secret-acs.yaml @@ -0,0 +1,159 @@ +- name: Get ACS details from ACS secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: roxsecrets + namespace: "{{ pipeline_namespace }}" + register: acs_secret + +- name: Get ACS Server URL from Secret + ansible.builtin.set_fact: + acs_url: "{{ acs_secret.resources.0.data.rox_central_endpoint | b64decode }}" + +- name: Get ACS Token Secret + ansible.builtin.set_fact: + acs_token: "{{ acs_secret.resources.0.data.rox_api_token | b64decode }}" + +- name: Get Cosign Secret Details + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "{{ secret_generate_name }}" + namespace: "{{ pipeline_namespace }}" + register: cosign_secret + +- name: Get Cosign Key from secret + ansible.builtin.set_fact: + cosign_key: "{{ cosign_secret.resources.0.data['cosign.key'] | b64decode | trim }}" + +- name: Get Cosign Password from secret + ansible.builtin.set_fact: + cosign_password: "{{ cosign_secret.resources.0.data['cosign.password'] | b64decode | trim }}" + +- name: Get Cosign Public Key from secret + ansible.builtin.set_fact: + cosign_pub: "{{ cosign_secret.resources.0.data['cosign.pub'] | b64decode | trim | replace('\n', '\\n')}}" + +- name: Get list of ACS Signatures + uri: + url: "https://{{ acs_url}}/v1/signatureintegrations" + headers: + Content-Type: application/json + method: GET + user: admin + password: "{{ stackrox_central_admin_password }}" + body_format: json + force_basic_auth: true + validate_certs: no + register: cosign_signature_list + +- name: Filter List of Integration Names + ansible.builtin.set_fact: + integration_names: "{{ cosign_signature_list.json.integrations | map(attribute='name') }}" + integration_ids: "{{ cosign_signature_list.json.integrations | map(attribute='id') }}" + when: cosign_signature_list.json.integrations | length>0 + +- name: Print List of Integrations + ansible.builtin.debug: + msg: "{{ integration_names }}" + when: cosign_signature_list.json.integrations | length>0 + +- name: Set signature ID + ansible.builtin.set_fact: + signature_id: "{% if item.0 == 'cosign-demo' %}{{item.1}}{% else %}''{% endif %}" + when: cosign_signature_list.json.integrations | length>0 + loop: "{{ integration_names|zip(integration_ids)|list }}" + +- name: Print signature ID + ansible.builtin.debug: + msg: "{{ signature_id }}" + when: cosign_signature_list.json.integrations | length>0 + +- name: Add Cosign Signature to ACS + uri: + url: "https://{{ acs_url }}/v1/signatureintegrations" + headers: + Content-Type: application/json + body: '{"id":"","name":"cosign-demo","cosign":{"publicKeys":[{"name":"cosign-demo","publicKeyPemEnc":"{{ cosign_pub }}"}]}}' + method: POST + user: admin + password: "{{ stackrox_central_admin_password }}" + body_format: json + force_basic_auth: true + validate_certs: no + register: cosign_signature_response + when: "'cosign-demo' not in integration_names|default('')" + +- name: Set signature ID + ansible.builtin.set_fact: + signature_id: "{{ cosign_signature_response.json.id }}" + when: cosign_signature_response.json|default("") != "" + +- name: Print signature ID + ansible.builtin.debug: + msg: "{{ signature_id }}" + when: cosign_signature_response.json|default("") != "" + +- name: Try Replace Hidden Characters in Signature ID + ansible.builtin.set_fact: + signature_id: "{{ signature_id | regex_replace('io\\.\\*+\\.([\\w\\.-]*)$','io.stackrox.\\1') }}" + ignore_errors: true + +- name: Print signature ID + ansible.builtin.debug: + msg: "{{ signature_id }}" + +- name: Replace SignatureId in our policy file' + ansible.builtin.replace: + path: "{{ role_path }}/files/policies/signed-image-policy.json" + regexp: 'io\.[\*\w]*\.[\w\.-]*' + replace: "{{ signature_id }}" + +- name: Apply/update policies + uri: + url: "https://{{ acs_url }}/v1/policies/import" + body: "{{ lookup('file', item ) }}" + method: POST + user: admin + password: "{{ stackrox_central_admin_password }}" + body_format: json + force_basic_auth: true + validate_certs: no + with_fileglob: + - "files/policies/*.json" + +- name: Check if Quay Secret Exists + k8s_info: + kind: Secret + name: "{{ quay_secret_name }}" + namespace: "{{ pipeline_namespace }}" + register: secret_check_status + +- name: extract quay hostname + shell: | + oc get route {{ quay_registry_name }}-quay -o jsonpath='{.spec.host}' -n {{ quay_project_name }} + register: quay_hostname_result + delay: 5 + retries: 10 + until: + - quay_hostname_result.stdout != "" + +- name: Set Quay hostname + ansible.builtin.set_fact: + quay_route: "{{ quay_hostname_result.stdout }}" + + +# - name: Add our Quay Registry as an Insecure Registry +# uri: +# url: "https://{{ acs_url }}/v1/imageintegrations" +# headers: +# Content-Type: application/json +# body: '{"id":"","name":"quay-demo-registry","categories":["REGISTRY","SCANNER"],"quay":{"endpoint":"{{ quay_route}}","oauthToken":"wewewewee","insecure":true},"autogenerated":false,"clusterId":"","clusters":[],"skipTestIntegration":true,"type":"quay"}' +# method: POST +# user: admin +# password: "{{ stackrox_central_admin_password }}" +# body_format: json +# force_basic_auth: true +# validate_certs: no +# register: cosign_signature_response +# when: "'cosign-demo' not in integration_names|default('')" \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/tasks/build-cosign-infra.yaml b/bootstrap/roles/ocp4-install-signing/tasks/build-cosign-infra.yaml new file mode 100644 index 0000000..9dca8a9 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/tasks/build-cosign-infra.yaml @@ -0,0 +1,78 @@ +- name: Create OpenShift Objects to build a pod to run Cosign Binary + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/cosign-ubi-is.yaml.j2 + - ./templates/cosign-is.yaml.j2 + - ./templates/cosign-build.yaml.j2 + +- name: Wait Until cosign build is complete + shell: | + oc get build -l build=cosign-pod -n "{{ pipeline_namespace }}" --sort-by=.metadata.creationTimestamp | tail -n 1 | awk '{print $4}' + register: build_status + retries: 10 + delay: 20 + until: + - build_status.stdout == "Complete" + +# - name: Make Sure Any Previously Generated Secrets are Absent +# kubernetes.core.k8s: +# state: absent +# api_version: v1 +# kind: Secret +# namespace: "{{ pipeline_namespace }}" +# name: "{{ secret_generate_name }}" +# wait: yes + +- name: Generate Random Cosign Password + set_fact: + cosign_password: "{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=6') }}" + +- name: Create OpenShift Objects for Cosign Usage + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: +# - ./templates/cosign-serviceaccount.yaml.j2 + - ./templates/cosign-role.yaml.j2 + - ./templates/cosign-rolebinding.yaml.j2 + - ./templates/cosign-rolebinding-cicd.yaml.j2 + - ./templates/cosign-deployment.yaml.j2 + +- name: Wait till Cosign Deployment is Ready + k8s_info: + kind: Deployment + name: cosign-pod + namespace: "{{ pipeline_namespace }}" + register: output_info + until: output_info.resources | json_query('[*].status.conditions[?reason==`NewReplicaSetAvailable`][].status') | select ('match','True') | list | length == 1 + delay: 5 + retries: 5 + +- name: Get Cosign Pod Name + shell: | + oc get pods -n "{{ pipeline_namespace }}" -l app=cosign-pod --sort-by=.metadata.creationTimestamp | tail -n 1| awk '{print $1}' + register: pod_name + +- name: Check if Cosign Signing Secret Already Exists + k8s_info: + kind: Secret + name: "{{ secret_generate_name }}" + namespace: "{{ pipeline_namespace }}" + register: secret_check_status + +- name: Create Cosign Secret + shell: | + oc exec pod/"{{ pod_name.stdout }}" -n "{{ pipeline_namespace }}" -- /bin/bash -c 'cosign generate-key-pair k8s://"{{ pipeline_namespace }}"/"{{ secret_generate_name }}"' + when: + secret_check_status.resources | length<1 + +- name: Confirm Cosign Secret is Created + kubernetes.core.k8s: + state: present + api_version: v1 + kind: Secret + namespace: "{{ pipeline_namespace }}" + name: "{{ secret_generate_name }}" + wait: yes \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/tasks/main.yaml b/bootstrap/roles/ocp4-install-signing/tasks/main.yaml new file mode 100644 index 0000000..cef6cb0 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/tasks/main.yaml @@ -0,0 +1,6 @@ +- import_tasks: build-cosign-infra.yaml +- import_tasks: add-cosign-secret-acs.yaml +- import_tasks: tektonchain.yaml +- import_tasks: patch-pipeline.yaml + + diff --git a/bootstrap/roles/ocp4-install-signing/tasks/patch-pipeline.yaml b/bootstrap/roles/ocp4-install-signing/tasks/patch-pipeline.yaml new file mode 100644 index 0000000..a3d71d9 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/tasks/patch-pipeline.yaml @@ -0,0 +1,85 @@ +- name: Check if Quay Secret Exists + k8s_info: + kind: Secret + name: "{{ quay_secret_name }}" + namespace: "{{ pipeline_namespace }}" + register: secret_check_status + +- name: extract quay hostname + shell: | + oc get route {{ quay_registry_name }}-quay -o jsonpath='{.spec.host}' -n {{ quay_project_name }} + register: quay_hostname_result + delay: 5 + retries: 10 + until: + - quay_hostname_result.stdout != "" + +- name: Set Quay hostname + ansible.builtin.set_fact: + quay_route: "{{ quay_hostname_result.stdout }}" + +- name: Link Quay Secret to Pipeline ServiceAccount for Pipeline Namespace + shell: | + oc secrets link pipeline "{{ quay_secret_name }}" --for=pull,mount -n {{ pipeline_namespace }} + +- name: Link Quay Secret to default accounts for namespaces that should pull from quay + shell: | + oc secrets link default "{{ quay_secret_name }}" --for=pull,mount -n {{ item }} + loop: "{{ secret_required_namespaces }}" + +- name: Create Quay Signing Task + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/cosign-task.yaml.j2 + +# Check Gogs +- name: Get gogs route + kubernetes.core.k8s_info: + kind: Route + api_version: route.openshift.io/v1 + namespace: cicd + name: gogs + register: r_gogs_route + retries: 10 + delay: 20 + until: + - r_gogs_route.resources[0].spec.host is defined + +- name: Debug gogs route + debug: + msg: "{{ r_gogs_route.resources[0].spec.host }}" + +- name: Create OpenShift Objects for Openshift Pipelines Templates + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/pipeline-build-dev.yaml.j2 + - ./templates/pipeline-build-stage.yaml.j2 +# - ./templates/pipeline-build-pvc.yaml.j2 + +- name: Patch Openshift Image Config - Message + ansible.builtin.debug: + msg: "As we did not generate a cert, we need to add our quay registry as an insecure registry in Openshift" + +- name: Patch Openshift Image Config + ansible.builtin.shell: | + oc patch image.config.openshift.io cluster --type=merge -p '{"spec":{"registrySources":{"insecureRegistries":["{{ quay_route }}"]}}}' + register: insecure_patch + +- name: Pause for 4 minutes to wait for MCP to recieve changes + ansible.builtin.pause: + minutes: 4 + when: "'no change' not in insecure_patch.stdout" + +# - name: Waiting Until Openshift Cluster is Ready from Patch +# ansible.builtin.shell: | +# oc get mcp -o json | jq '.items[] | select (.status.machineCount==.status.updatedMachineCount)' +# register: mcp_result +# delay: 10 +# retries: 15 +# until: +# - mcp_result.stdout != "" +# when: "'no change' not in insecure_patch.stdout" \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/tasks/tektonchain.yaml b/bootstrap/roles/ocp4-install-signing/tasks/tektonchain.yaml new file mode 100644 index 0000000..73c7a95 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/tasks/tektonchain.yaml @@ -0,0 +1,94 @@ +- name: Delete Operator Namespace if it Exists + kubernetes.core.k8s: + state: absent + api_version: project.openshift.io/v1 + kind: Project + name: "{{ tekton_operator_namespace }}" + wait: yes + +#Tekton Manifest Based Installation -https://github.com/tektoncd/chains#installation +- name: Install Tekton Chains via Manifest + shell: | + oc apply -f https://storage.googleapis.com/tekton-releases/chains/previous/"{{ tekton_chain_version }}"/release.yaml + when: tekton_install_type == "manifest" + +#Tekton CR Based Installation - https://docs.openshift.com/container-platform/4.10/cicd/pipelines/using-tekton-chains-for-openshift-pipelines-supply-chain-security.html +- name: Install Tekton Chains via Pipeline CR + k8s: + state: present + definition: "{{ lookup('template', item ) | from_yaml }}" + loop: + - ./templates/tektonchain-cr.yaml.j2 + when: tekton_install_type != "manifest" + +- name: Apply TektonChain Controller SCC + shell: | + oc project {{ tekton_operator_namespace }} && oc adm policy add-scc-to-user nonroot -z tekton-chains-controller + +# Wait Until TektonChain Controller Pod Is Ready +- name: Get TektonChain Pod + kubernetes.core.k8s_info: + kind: Pod + api_version: v1 + namespace: "{{ tekton_operator_namespace }}" + label_selectors: + - app = tekton-chains-controller + wait: yes + retries: 3 + delay: 20 + +# - name: Check if Cosign Signing Secret Already Exists in Operator Namespace +# k8s_info: +# kind: Secret +# name: "{{ secret_generate_name }}" +# namespace: "{{ tekton_operator_namespace }}" +# register: secret_check_status_operator + +- name: Delete Cosign Secret if exists + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + namespace: "{{ tekton_operator_namespace }}" + name: "{{ secret_generate_name }}" + wait: yes + +- name: Check if Cosign Signing Secret Already Exists in Pipeline Namespace + k8s_info: + kind: Secret + name: "{{ secret_generate_name }}" + namespace: "{{ pipeline_namespace }}" + register: secret_check_status_pipeline + +- name: Print Secret from Pipeline Namespace if it Exists + ansible.builtin.debug: + msg: "{{ secret_check_status_pipeline.resources }}" + +- name: Copy Secret from Pipeline Namespace if it Exists + ansible.builtin.shell: | + oc get secret {{ secret_generate_name }} -n {{ pipeline_namespace }} -o json | jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid,.metadata.selfLink,.metadata.managedFields,.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") | .metadata.creationTimestamp=null | .metadata.name="{{ secret_generate_name }}"'| oc apply -n {{ tekton_operator_namespace }} -f - + when: + - secret_check_status_pipeline.resources | default([]) | length>0 + +- name: Create Cosign Secret + shell: | + oc exec pod/"{{ pod_name.stdout }}" -n "{{ pipeline_namespace }}" -- /bin/bash -c 'cosign generate-key-pair k8s://"{{ pipeline_namespace }}"/"{{ secret_generate_name }}"' + when: + - secret_check_status_pipeline.resources | default([]) | length<1 + +- name: Confirm Cosign Secret is Created + kubernetes.core.k8s: + state: present + api_version: v1 + kind: Secret + namespace: "{{ pipeline_namespace }}" + name: "{{ secret_generate_name }}" + wait: yes + +- name: Patch the CM of Openshift Piplines to enable Tekton Chains + command: oc patch configmap chains-config -n {{ tekton_operator_namespace }} -p='{"data":{'\"{{ item.key }}\"':'\"{{ item.value }}\"'}}' + loop: "{{ tekton_chain_keys | dict2items }}" + +- name: Recreate Tekton Chain Controller + shell: | + oc delete po -l app=tekton-chains-controller -n "{{ tekton_operator_namespace }}" \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-anyuid-scc.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-anyuid-scc.yaml.j2 new file mode 100644 index 0000000..2b13da0 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-anyuid-scc.yaml.j2 @@ -0,0 +1,12 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cosign-sa-anyuid-scc +subjects: + - kind: ServiceAccount + name: cosign-sa + namespace: {{ tekton_operator_namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: 'system:openshift:scc:anyuid' diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-build.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-build.yaml.j2 new file mode 100644 index 0000000..7b2f185 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-build.yaml.j2 @@ -0,0 +1,62 @@ +kind: BuildConfig +apiVersion: build.openshift.io/v1 +metadata: + name: cosign-pod + namespace: {{ pipeline_namespace }} + labels: + build: cosign-pod +spec: + nodeSelector: null + output: + to: + kind: ImageStreamTag + name: 'cosign-pod:latest' + resources: {} + successfulBuildsHistoryLimit: 5 + failedBuildsHistoryLimit: 5 + strategy: + type: Docker + dockerStrategy: + from: + kind: ImageStreamTag + name: 'ubi:8.0' + postCommit: {} + source: + type: Dockerfile + dockerfile: >- + FROM registry.redhat.io/ubi8/ubi:8.0 + + + RUN yum install go git wget tar rsync -y && wget + https://github.com/sigstore/cosign/releases/download/v1.9.0/cosign-1.9.0.x86_64.rpm + \ + && yum localinstall -y cosign-1.9.0.x86_64.rpm \ + && wget https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/openshift-client-linux.tar.gz \ + && tar -xvf openshift-client-linux.tar.gz && mv oc /usr/local/bin \ + && rm -rf openshift-client-linux.tar.gz \ + && rm -rf cosign-1.9.0.x86_64.rpm \ + && mkdir /.docker \ + && mkdir -p /tekton/home \ + && mkdir /workdir + + USER 0 + + RUN chgrp -R 0 /usr/bin && \ + chmod -R g=u /usr/bin && \ + chgrp -R 0 /.docker && \ + chmod -R g=u /.docker && \ + chgrp -R 0 /tekton/home && \ + chmod -R g=u /tekton/home && \ + chgrp -R 0 /workdir && \ + chmod -R g=u /workdir + + WORKDIR /workdir + + USER 1001 + + ENTRYPOINT cosign + triggers: + - type: ConfigChange + - type: ImageChange + imageChange: {} + runPolicy: Serial diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-deployment.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-deployment.yaml.j2 new file mode 100644 index 0000000..846bd69 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-deployment.yaml.j2 @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cosign-pod + namespace: {{ pipeline_namespace }} +spec: + selector: + matchLabels: + app: cosign-pod + replicas: 1 + template: + metadata: + labels: + app: cosign-pod + spec: + containers: + - name: cosign-pod + image: {{cosign_image}} + env: + - name: COSIGN_PASSWORD + value: {{ cosign_password }} + command: + - '/bin/bash' + args: + - -c + - sleep infinity + serviceAccountName: pipeline + serviceAccount: pipeline diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-is.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-is.yaml.j2 new file mode 100644 index 0000000..52d6292 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-is.yaml.j2 @@ -0,0 +1,13 @@ +kind: ImageStream +apiVersion: image.openshift.io/v1 +metadata: + name: cosign-pod + namespace: {{ pipeline_namespace }} + labels: + build: cosign-pod +spec: + lookupPolicy: + local: false +status: + dockerImageRepository: >- + image-registry.openshift-image-registry.svc:5000/{{ pipeline_namespace }}/cosign-pod diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-role.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-role.yaml.j2 new file mode 100644 index 0000000..887433d --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-role.yaml.j2 @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cosign-role + namespace: {{ pipeline_namespace }} +rules: + - verbs: + - '*' + apiGroups: + - '' + resources: + - secrets \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding-cicd.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding-cicd.yaml.j2 new file mode 100644 index 0000000..c3d5e21 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding-cicd.yaml.j2 @@ -0,0 +1,13 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cosign-sa-edit + namespace: {{ pipeline_namespace }} +subjects: + - kind: ServiceAccount + name: pipeline + namespace: {{ pipeline_namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: edit diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding.yaml.j2 new file mode 100644 index 0000000..c118711 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-rolebinding.yaml.j2 @@ -0,0 +1,13 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cosign-rolebinding + namespace: {{ pipeline_namespace }} +subjects: + - kind: ServiceAccount + name: cosign-sa + namespace: {{ pipeline_namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cosign-role diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-serviceaccount.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-serviceaccount.yaml.j2 new file mode 100644 index 0000000..fb87e7a --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-serviceaccount.yaml.j2 @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cosign-sa + namespace: {{ tekton_operator_namespace }} diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-task.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-task.yaml.j2 new file mode 100644 index 0000000..447d95d --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-task.yaml.j2 @@ -0,0 +1,36 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: cosign-task + namespace: {{ pipeline_namespace }} + labels: + app.kubernetes.io/version: "0.6" + annotations: + tekton.dev/pipelines.minVersion: "0.17.0" + tekton.dev/categories: security + tekton.dev/tags: security + tekton.dev/displayName: "Sign and Verify Images" + tekton.dev/platforms: "linux/amd64" +spec: + description: >- + This Task can be used to sign an image in a registry + params: + - name: IMAGE + description: Name (reference) of the cosign image + - name: SIGNATURE_IMAGE + description: Image to be signed + - name: ARGS + type: array + description: args to pass to cosign command to + default: [] + steps: + - name: cosign-actions + image: $(params.IMAGE) + script: | + #!/usr/bin/env bash + cmd="cosign $* $(params.SIGNATURE_IMAGE)" + echo "Starting Image Signing Task" + echo "This is the command we will run $cmd" + $cmd + args: + - "$(params.ARGS)" diff --git a/bootstrap/roles/ocp4-install-signing/templates/cosign-ubi-is.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/cosign-ubi-is.yaml.j2 new file mode 100644 index 0000000..ff84878 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/cosign-ubi-is.yaml.j2 @@ -0,0 +1,21 @@ +kind: ImageStream +apiVersion: image.openshift.io/v1 +metadata: + name: ubi + namespace: {{ pipeline_namespace }} + labels: + build: cosign-pod +spec: + lookupPolicy: + local: false + tags: + - name: '8.0' + annotations: + openshift.io/imported-from: 'registry.redhat.io/ubi8/ubi:8.0' + from: + kind: DockerImage + name: 'registry.redhat.io/ubi8/ubi:8.0' + generation: 2 + importPolicy: {} + referencePolicy: + type: Source \ No newline at end of file diff --git a/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-dev.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-dev.yaml.j2 new file mode 100644 index 0000000..b96e56c --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-dev.yaml.j2 @@ -0,0 +1,279 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: petclinic-build-dev + namespace: cicd +spec: + params: + - name: APP_SOURCE_GIT + type: string + description: The application git repository + default: http://{{ r_gogs_route.resources[0].spec.host }}/gogs/spring-petclinic + - name: APP_SOURCE_REVISION + type: string + description: The application git revision + default: master + - name: APP_MANIFESTS_GIT + type: string + description: The application manifests git repository + default: http://{{ r_gogs_route.resources[0].spec.host }}/gogs/spring-petclinic-config + - name: APP_IMAGE_TAG + type: string + default: latest + description: The application image tag to build + - name: DEV_NAMESPACE + type: string + default: devsecops-dev + description: The namespace for Stage environments + - name: APP_TESTS_GIT + type: string + description: The application test cases git repository + default: https://github.com/rcarrata/spring-petclinic-gatling + workspaces: + - name: workspace + - name: maven-settings + tasks: + - name: source-clone + taskRef: + name: git-clone + kind: ClusterTask + workspaces: + - name: output + workspace: workspace + params: + - name: url + value: $(params.APP_SOURCE_GIT) + - name: revision + value: $(params.APP_SOURCE_REVISION) + - name: depth + value: "0" + - name: subdirectory + value: spring-petclinic + - name: deleteExisting + value: "true" + - name: unit-tests + taskRef: + name: maven + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: ["package", "-f", "spring-petclinic"] + - name: code-analysis + taskRef: + name: maven + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: + - install + - sonar:sonar + - -f + - spring-petclinic + - -Dsonar.host.url=http://sonarqube:9000 + - -Dsonar.userHome=/tmp/sonar + - -DskipTests=true + - name: dependency-report + taskRef: + name: dependency-report + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: SOURCE_DIR + value: spring-petclinic + - name: release-app + taskRef: + name: maven + runAfter: + - code-analysis + - unit-tests + - dependency-report + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: + - deploy + - -f + - spring-petclinic + - -DskipTests=true + - -DaltDeploymentRepository=nexus::default::http://nexus:8081/repository/maven-releases/ + - -DaltSnapshotDeploymentRepository=nexus::default::http://nexus:8081/repository/maven-snapshots/ + - name: build-image + taskRef: + name: s2i-java-11 + runAfter: + - release-app + params: + - name: TLSVERIFY + value: "false" + - name: MAVEN_MIRROR_URL + value: http://nexus:8081/repository/maven-public/ + - name: PATH_CONTEXT + value: spring-petclinic/target + - name: IMAGE_NAME + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-dev + - name: IMAGE_TAG + value: $(params.APP_IMAGE_TAG) + workspaces: + - name: source + workspace: workspace + - name: image-sign + taskRef: + name: cosign-task + runAfter: + - build-image + params: + - name: IMAGE + value: "image-registry.openshift-image-registry.svc:5000/{{pipeline_namespace}}/cosign-pod" + - name: SIGNATURE_IMAGE + value: "{{ quay_route}}/{{ quay_org_name }}/spring-petclinic-dev" + - name: ARGS + value: + - "sign" + - "--allow-insecure-registry" + - "--key k8s://{{ pipeline_namespace }}/{{ secret_generate_name }}" + - name: image-scan + runAfter: + - build-image + taskRef: + name: rox-image-scan + kind: ClusterTask + params: + - name: image + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-dev + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: output_format + value: table + - name: image_digest + value: $(tasks.build-image.results.IMAGE_DIGEST) + - name: image-check + runAfter: + - build-image + taskRef: + name: rox-image-check + kind: ClusterTask + params: + - name: image + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-dev + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: image_digest + value: $(tasks.build-image.results.IMAGE_DIGEST) + - name: deploy-check + runAfter: + - build-image + taskRef: + name: rox-deployment-check + kind: ClusterTask + params: + - name: GIT_REPOSITORY + value: "$(params.APP_MANIFESTS_GIT)" + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: file + value: deployment.yaml + - name: deployment_files_path + value: app + workspaces: + - name: workspace + workspace: workspace + - name: update-deployment + runAfter: + - image-sign + - image-scan + - image-check + - deploy-check + taskRef: + name: git-update-deployment + params: + - name: GIT_REPOSITORY + value: "$(params.APP_MANIFESTS_GIT)" + - name: GIT_USERNAME + value: gogs + - name: GIT_PASSWORD + value: gogs + - name: CURRENT_IMAGE + value: quay.io/siamaksade/spring-petclinic:latest + - name: NEW_IMAGE + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-dev + - name: NEW_DIGEST + value: "$(tasks.build-image.results.IMAGE_DIGEST)" + - name: KUSTOMIZATION_PATH + value: environments/dev + workspaces: + - name: workspace + workspace: workspace + - name: wait-application + taskRef: + name: argocd-task-sync-and-wait + runAfter: + - update-deployment + params: + - name: application-name + value: dev-spring-petclinic + - name: perf-tests-clone + taskRef: + name: git-clone + kind: ClusterTask + workspaces: + - name: output + workspace: workspace + runAfter: + - wait-application + params: + - name: url + value: $(params.APP_TESTS_GIT) + - name: subdirectory + value: spring-petclinic-gatling + - name: deleteExisting + value: "true" + - name: pentesting-test + taskRef: + name: zap-proxy + runAfter: + - perf-tests-clone + params: + - name: APP_URL + value: "http://spring-petclinic.$(params.DEV_NAMESPACE).svc.cluster.local:8080" + workspaces: + - name: workspace + workspace: workspace + - name: performance-test + taskRef: + name: gatling + runAfter: + - perf-tests-clone + params: + - name: APP_URL + value: "http://spring-petclinic.$(params.DEV_NAMESPACE).svc.cluster.local:8080" + workspaces: + - name: simulations + workspace: workspace + subPath: spring-petclinic-gatling diff --git a/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-stage.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-stage.yaml.j2 new file mode 100644 index 0000000..f056670 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/pipeline-build-stage.yaml.j2 @@ -0,0 +1,279 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: petclinic-build-stage + namespace: cicd +spec: + params: + - name: APP_SOURCE_GIT + type: string + description: The application git repository + default: http://{{ r_gogs_route.resources[0].spec.host }}/gogs/spring-petclinic + - name: APP_SOURCE_REVISION + type: string + description: The application git revision + default: master + - name: APP_MANIFESTS_GIT + type: string + description: The application manifests git repository + default: http://{{ r_gogs_route.resources[0].spec.host }}/gogs/spring-petclinic-config + - name: APP_IMAGE_TAG + type: string + default: latest + description: The application image tag to build + - name: STAGE_NAMESPACE + type: string + default: devsecops-qa + description: The namespace for Stage environments + - name: APP_TESTS_GIT + type: string + description: The application test cases git repository + default: https://github.com/rcarrata/spring-petclinic-gatling + workspaces: + - name: workspace + - name: maven-settings + tasks: + - name: source-clone + taskRef: + name: git-clone + kind: ClusterTask + workspaces: + - name: output + workspace: workspace + params: + - name: url + value: $(params.APP_SOURCE_GIT) + - name: revision + value: $(params.APP_SOURCE_REVISION) + - name: depth + value: "0" + - name: subdirectory + value: spring-petclinic + - name: deleteExisting + value: "true" + - name: unit-tests + taskRef: + name: maven + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: ["package", "-f", "spring-petclinic"] + - name: code-analysis + taskRef: + name: maven + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: + - install + - sonar:sonar + - -f + - spring-petclinic + - -Dsonar.host.url=http://sonarqube:9000 + - -Dsonar.userHome=/tmp/sonar + - -DskipTests=true + - name: dependency-report + taskRef: + name: dependency-report + runAfter: + - source-clone + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: SOURCE_DIR + value: spring-petclinic + - name: release-app + taskRef: + name: maven + runAfter: + - code-analysis + - unit-tests + - dependency-report + workspaces: + - name: source + workspace: workspace + - name: maven-settings + workspace: maven-settings + params: + - name: GOALS + value: + - deploy + - -f + - spring-petclinic + - -DskipTests=true + - -DaltDeploymentRepository=nexus::default::http://nexus:8081/repository/maven-releases/ + - -DaltSnapshotDeploymentRepository=nexus::default::http://nexus:8081/repository/maven-snapshots/ + - name: build-image + taskRef: + name: s2i-java-11 + runAfter: + - release-app + params: + - name: TLSVERIFY + value: "false" + - name: MAVEN_MIRROR_URL + value: http://nexus:8081/repository/maven-public/ + - name: PATH_CONTEXT + value: spring-petclinic/target + - name: IMAGE_NAME + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-stage + - name: IMAGE_TAG + value: $(params.APP_IMAGE_TAG) + workspaces: + - name: source + workspace: workspace + - name: image-sign + taskRef: + name: cosign-task + runAfter: + - build-image + params: + - name: IMAGE + value: "image-registry.openshift-image-registry.svc:5000/{{pipeline_namespace}}/cosign-pod" + - name: SIGNATURE_IMAGE + value: "{{ quay_route}}/{{ quay_org_name }}/spring-petclinic-stage" + - name: ARGS + value: + - "sign" + - "--allow-insecure-registry" + - "--key k8s://{{ pipeline_namespace }}/{{ secret_generate_name }}" + - name: image-scan + runAfter: + - build-image + taskRef: + name: rox-image-scan + kind: ClusterTask + params: + - name: image + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-stage + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: output_format + value: table + - name: image_digest + value: $(tasks.build-image.results.IMAGE_DIGEST) + - name: image-check + runAfter: + - build-image + taskRef: + name: rox-image-check + kind: ClusterTask + params: + - name: image + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-stage + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: image_digest + value: $(tasks.build-image.results.IMAGE_DIGEST) + - name: deploy-check + runAfter: + - build-image + taskRef: + name: rox-deployment-check + kind: ClusterTask + params: + - name: GIT_REPOSITORY + value: "$(params.APP_MANIFESTS_GIT)" + - name: rox_api_token + value: roxsecrets + - name: rox_central_endpoint + value: roxsecrets + - name: file + value: deployment.yaml + - name: deployment_files_path + value: app + workspaces: + - name: workspace + workspace: workspace + - name: update-deployment + runAfter: + - image-sign + - image-scan + - image-check + - deploy-check + taskRef: + name: git-update-deployment + params: + - name: GIT_REPOSITORY + value: "$(params.APP_MANIFESTS_GIT)" + - name: GIT_USERNAME + value: gogs + - name: GIT_PASSWORD + value: gogs + - name: CURRENT_IMAGE + value: quay.io/siamaksade/spring-petclinic:latest + - name: NEW_IMAGE + value: {{ quay_route}}/{{ quay_org_name }}/spring-petclinic-stage + - name: NEW_DIGEST + value: "$(tasks.build-image.results.IMAGE_DIGEST)" + - name: KUSTOMIZATION_PATH + value: environments/stage + workspaces: + - name: workspace + workspace: workspace + - name: wait-application + taskRef: + name: argocd-task-sync-and-wait + runAfter: + - update-deployment + params: + - name: application-name + value: stage-spring-petclinic + - name: perf-tests-clone + taskRef: + name: git-clone + kind: ClusterTask + workspaces: + - name: output + workspace: workspace + runAfter: + - wait-application + params: + - name: url + value: $(params.APP_TESTS_GIT) + - name: subdirectory + value: spring-petclinic-gatling + - name: deleteExisting + value: "true" + - name: pentesting-test + taskRef: + name: zap-proxy + runAfter: + - perf-tests-clone + params: + - name: APP_URL + value: "http://spring-petclinic.$(params.STAGE_NAMESPACE).svc.cluster.local:8080" + workspaces: + - name: workspace + workspace: workspace + - name: performance-test + taskRef: + name: gatling + runAfter: + - perf-tests-clone + params: + - name: APP_URL + value: "http://spring-petclinic.$(params.STAGE_NAMESPACE).svc.cluster.local:8080" + workspaces: + - name: simulations + workspace: workspace + subPath: spring-petclinic-gatling diff --git a/bootstrap/roles/ocp4-install-signing/templates/tektonchain-cr.yaml.j2 b/bootstrap/roles/ocp4-install-signing/templates/tektonchain-cr.yaml.j2 new file mode 100644 index 0000000..3d30b76 --- /dev/null +++ b/bootstrap/roles/ocp4-install-signing/templates/tektonchain-cr.yaml.j2 @@ -0,0 +1,6 @@ +apiVersion: operator.tekton.dev/v1alpha1 +kind: TektonChain +metadata: + name: chain +spec: + targetNamespace: {{ tekton_operator_namespace }} \ No newline at end of file diff --git a/demo.sh b/demo.sh index 04c5809..d394342 100755 --- a/demo.sh +++ b/demo.sh @@ -20,7 +20,7 @@ err() { while (( "$#" )); do case "$1" in - start|promote|status) + start|promote|status|sign-verify) COMMAND=$1 shift ;; @@ -48,6 +48,7 @@ command.help() { start Starts the deploy DEV pipeline promote Starts the deploy STAGE pipeline status Check the resources available for the demo + sign-verify If Tekton Chaining was enabled run Signature Verification On Tasks help Help about this command EOF } @@ -101,6 +102,51 @@ command.promote() { echo "" } +command.sign-verify() { + info "## Will attempt to verify TaskRuns of Last Pipelinerun" + tekton_chain_namespaces=("cicd") + working_namespace="" + verify_script="verify-pipeline.sh" + + for namespace in ${tekton_chain_namespaces[@]} + do + pod_check=$(oc get pods -n $namespace -l app=cosign-pod 2>&1) + if echo ${pod_check} | grep -iv "No resources found" + then + working_namespace=$namespace + cosign_pod=$(oc get pods -n $namespace -l app=cosign-pod --sort-by=.metadata.creationTimestamp | tail -n 1| awk '{print $1}') + fi + done + + if [ -z "${cosign_pod:-}" ] + then + info "## Cosign Pod not Found,Please make sure to run the deploy_signing.yaml" + exit 1 + else + info "## Cosign Pod(${cosign_pod}) found in namespace:${namespace}" + fi + + info "## Copying Verification Script to Cosign Pod(${cosign_pod})" + oc rsync --include=${verify_script} -n $working_namespace ./run/verify $cosign_pod:/workdir > /dev/null + + info "## Attemtpting to run verification script in Pod(${cosign_pod})" + oc exec pod/"$cosign_pod" -n $working_namespace -- /bin/bash -c "chmod ugo+x /workdir/verify/${verify_script}" + oc exec pod/"$cosign_pod" -n $working_namespace -- /bin/bash -c "/workdir/verify/${verify_script} $working_namespace " + + # echo "Obtaining cosign.key" + # oc exec pod/"$cosign_pod" -n openshift-pipelines -- /bin/bash -c "oc get secret/signing-secrets -n openshift-pipelines -o jsonpath='{.data.cosign\.key}' | base64 -d > /test/cosign.key" + # echo "Obtaining cosign.password" + # oc exec pod/"$cosign_pod" -n openshift-pipelines -- /bin/bash -c "oc get secret/signing-secrets -n openshift-pipelines -o jsonpath='{.data.cosign\.password}' | base64 -d > /test/cosign.password" + # echo "Obtaining cosign public key" + # oc exec pod/"$cosign_pod" -n openshift-pipelines -- /bin/bash -c "oc get secret/signing-secrets -n openshift-pipelines -o jsonpath='{.data.cosign\.pub}' | base64 -d > /test/cosign.pub" + + + # signature=$(oc get taskrun/petclinic-build-dev-z8zq7v-build-image -n cicd -o jsonpath='{.metadata.annotations.chains\.tekton\.dev/signature-taskrun-171087b9-512e-4237-ab70-6f01083e9170}') + # payload=$(oc get taskrun/petclinic-build-dev-z8zq7v-build-image -n cicd -o jsonpath='{.metadata.annotations.chains\.tekton\.dev/payload-taskrun-171087b9-512e-4237-ab70-6f01083e9170}') + + # oc run cosign-verify --image=gcr.io/projectsigstore/cosign:v1.9.0 -- verify --key $cosign_key --signature $signature $payload +} + main() { local fn="command.$COMMAND" valid_command "$fn" || { diff --git a/docs/pics/acs-trusted-signature-violation.png b/docs/pics/acs-trusted-signature-violation.png new file mode 100644 index 0000000..01b9fd7 Binary files /dev/null and b/docs/pics/acs-trusted-signature-violation.png differ diff --git a/docs/pics/pipeline-with-sign-task.png b/docs/pics/pipeline-with-sign-task.png new file mode 100644 index 0000000..35194ce Binary files /dev/null and b/docs/pics/pipeline-with-sign-task.png differ diff --git a/docs/pics/quay-with-signatures.png b/docs/pics/quay-with-signatures.png new file mode 100644 index 0000000..64ac6df Binary files /dev/null and b/docs/pics/quay-with-signatures.png differ diff --git a/docs/pics/taskrun.png b/docs/pics/taskrun.png new file mode 100644 index 0000000..3bed96f Binary files /dev/null and b/docs/pics/taskrun.png differ diff --git a/extend.sh b/extend.sh new file mode 100755 index 0000000..2bcd94e --- /dev/null +++ b/extend.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e -u -o pipefail + +valid_command() { + local fn=$1; shift + [[ $(type -t "$fn") == "function" ]] +} + +info() { + printf "\n# INFO: $@\n" +} + +err() { + printf "\n# ERROR: $1\n" + exit 1 +} + +info "Installing Demo" +ansible-playbook bootstrap/deploy_signing.yaml -v \ No newline at end of file diff --git a/run/verify/verify-pipeline.sh b/run/verify/verify-pipeline.sh new file mode 100644 index 0000000..57f1315 --- /dev/null +++ b/run/verify/verify-pipeline.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +echo "## Verify Starting Script" + +tekton_secret_namespace="${1:-openshift-pipelines}" +cosign_secret_name="${2:-signing-secrets}" +pipeline_namespace="${3:-cicd}" +writable_space="${4:-/workdir}" +pipeline_label="${5:-petclinic-build-dev}" + +#Can get Secrets Directly but will depend on Kubernetes Permission +# if [ ! -f ${writable_space}/cosign.key ] +# then +# oc get secret/${cosign_secret_name} -n ${tekton_secret_namespace} -o jsonpath='{.data.cosign\.key}' | base64 -d > ${writable_space}/cosign.key +# fi + +# if [ ! -f ${writable_space}/cosign.password ] +# then +# oc get secret/${cosign_secret_name} -n ${tekton_secret_namespace} -o jsonpath='{.data.cosign\.password}' | base64 -d > ${writable_space}/cosign.password +# fi + +# if [ ! -f ${writable_space}/cosign.pub ] +# then +# oc get secret/${cosign_secret_name} -n ${tekton_secret_namespace} -o jsonpath='{.data.cosign\.pub}' | base64 -d > ${writable_space}/cosign.pub +# fi + +printf "Getting Most Recent Pipeline Run for ${pipeline_label}\n" +pipelinerun=$(oc get pipelinerun -n ${pipeline_namespace} -l tekton.dev/pipeline=${pipeline_label} --sort-by=.metadata.creationTimestamp | tail -n 1| awk '{print $1}') +printf "Found Pipeline Run ${pipelinerun}\n\n" + +printf "Will attempt to Verify Task Runs for Pipeline Run ${pipelinerun}\n" +for taskrun in $(oc get taskrun -n ${pipeline_namespace} -l tekton.dev/pipelineRun=${pipelinerun} -o name) +do + printf "\n" + printf "Start Verification of TaskRun ${taskrun} in PipelineRun ${pipelinerun}\n" + TASKRUN_UID=$(oc get $taskrun -n ${pipeline_namespace} -o jsonpath='{.metadata.uid}') + oc get $taskrun -n ${pipeline_namespace} -o jsonpath="{.metadata.annotations.chains\.tekton\.dev/signature-taskrun-$TASKRUN_UID}" > ${writable_space}/signature + oc get $taskrun -n ${pipeline_namespace} -o jsonpath="{.metadata.annotations.chains\.tekton\.dev/payload-taskrun-$TASKRUN_UID}" | base64 -d > ${writable_space}/payload + cosign verify-blob -d --key k8s://${tekton_secret_namespace}/${cosign_secret_name} --signature ${writable_space}/signature ${writable_space}/payload + if [ "$?" -eq 0 ] + then + printf "Verified TaskRun ${taskrun} in PipelineRun ${pipelinerun}\n" + printf "\n" + else + printf "Could not verify TaskRun ${taskrun} in PipelineRun ${pipelinerun}" + printf "\n" + fi +done +