diff --git a/infrastructure/files/cosign-hook/cosign.sh b/infrastructure/files/cosign-hook/cosign.sh new file mode 100644 index 0000000..6258f11 --- /dev/null +++ b/infrastructure/files/cosign-hook/cosign.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -Eeuo pipefail + +mkdir -p /tmp/bin +curl -sfLo /tmp/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.0.2/cosign-linux-amd64 +chmod 755 /tmp/bin/cosign +export PATH="/tmp/bin:$PATH" + +if ! oc get secret code-signature -n fruits-dev &>/dev/null; then + echo "========================================================================" + echo " Generating a keypair" + echo "========================================================================" + echo + + ## 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 diff --git a/infrastructure/files/stackrox-configure-hook/configure.yaml b/infrastructure/files/stackrox-configure-hook/configure.yaml new file mode 100644 index 0000000..8c0f18f --- /dev/null +++ b/infrastructure/files/stackrox-configure-hook/configure.yaml @@ -0,0 +1,186 @@ +- 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 diff --git a/infrastructure/files/stackrox-configure-hook/entrypoint.sh b/infrastructure/files/stackrox-configure-hook/entrypoint.sh new file mode 100644 index 0000000..f6abc66 --- /dev/null +++ b/infrastructure/files/stackrox-configure-hook/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -Eeuo pipefail + +ansible-galaxy collection install community.general +ansible-playbook configure.yaml + +exit 0 diff --git a/infrastructure/files/stackrox-init-hook/configure-acs.sh b/infrastructure/files/stackrox-init-hook/configure-acs.sh new file mode 100644 index 0000000..9a3b646 --- /dev/null +++ b/infrastructure/files/stackrox-init-hook/configure-acs.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -Eeuo pipefail + +mkdir -p /tmp/bin +curl -sfLo /tmp/bin/roxctl https://mirror.openshift.com/pub/rhacs/assets/4.0.0/bin/Linux/roxctl +chmod 755 /tmp/bin/roxctl +curl -sLo /tmp/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 +chmod 755 /tmp/bin/jq +export PATH="/tmp/bin:$PATH" + +echo "========================================================================" +echo " Connecting to Red Hat ACS" +echo "========================================================================" +echo + +export ROX_CENTRAL_ADDRESS="$(oc get route central -n stackrox -o go-template='{{.spec.host}}'):443" +export ROX_CENTRAL_HOSTNAME="$ROX_CENTRAL_ADDRESS" +while ! curl -sfko /dev/null "https://$ROX_CENTRAL_ADDRESS/"; do + echo "Red Hat ACS not ready..." + sleep 5 +done + +echo "========================================================================" +echo " Retrieving an API Token for Red Hat ACS" +echo "========================================================================" +echo +if ! oc get secret stackrox-api-token -n stackrox &>/dev/null; then + POLICY_JSON='{ "name": "init-token", "role":"Admin"}' + APIURL="https://$ROX_CENTRAL_ADDRESS/v1/apitokens/generate" + export ROX_API_TOKEN=$(curl -s -k -u admin:$ROX_ADMIN_PASSWORD -H 'Content-Type: application/json' -X POST -d "$POLICY_JSON" "$APIURL" | jq -r '.token') + oc create secret generic stackrox-api-token -n stackrox --from-literal=token="$ROX_API_TOKEN" +else + export ROX_API_TOKEN="$(oc get secret stackrox-api-token -n stackrox -o go-template --template='{{.data.token|base64decode}}')" +fi + +echo "========================================================================" +echo " Generating the Cluster Init Bundle" +echo "========================================================================" +echo + +if ! oc get secret admission-control-tls -n stackrox &>/dev/null; then + roxctl -e "$ROX_CENTRAL_ADDRESS" central init-bundles generate local-cluster --output-secrets /tmp/cluster_init_bundle.yaml + oc apply -f /tmp/cluster_init_bundle.yaml -n stackrox +fi + +exit 0 diff --git a/infrastructure/templates/acs.yaml b/infrastructure/templates/acs.yaml index e4320bf..5bdb52d 100644 --- a/infrastructure/templates/acs.yaml +++ b/infrastructure/templates/acs.yaml @@ -166,54 +166,7 @@ metadata: name: stackrox-init-hook namespace: stackrox data: - configure-acs.sh: | - #!/bin/bash - - set -Eeuo pipefail - - mkdir -p /tmp/bin - curl -sfLo /tmp/bin/roxctl https://mirror.openshift.com/pub/rhacs/assets/4.0.0/bin/Linux/roxctl - chmod 755 /tmp/bin/roxctl - curl -sLo /tmp/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 - chmod 755 /tmp/bin/jq - export PATH="/tmp/bin:$PATH" - - echo "========================================================================" - echo " Connecting to Red Hat ACS" - echo "========================================================================" - echo - - export ROX_CENTRAL_ADDRESS="$(oc get route central -n stackrox -o go-template='{{ "{{" }}.spec.host}}'):443" - export ROX_CENTRAL_HOSTNAME="$ROX_CENTRAL_ADDRESS" - while ! curl -sfko /dev/null "https://$ROX_CENTRAL_ADDRESS/"; do - echo "Red Hat ACS not ready..." - sleep 5 - done - - echo "========================================================================" - echo " Retrieving an API Token for Red Hat ACS" - echo "========================================================================" - echo - if ! oc get secret stackrox-api-token -n stackrox &>/dev/null; then - POLICY_JSON='{ "name": "init-token", "role":"Admin"}' - APIURL="https://$ROX_CENTRAL_ADDRESS/v1/apitokens/generate" - export ROX_API_TOKEN=$(curl -s -k -u admin:$ROX_ADMIN_PASSWORD -H 'Content-Type: application/json' -X POST -d "$POLICY_JSON" "$APIURL" | jq -r '.token') - oc create secret generic stackrox-api-token -n stackrox --from-literal=token="$ROX_API_TOKEN" - else - export ROX_API_TOKEN="$(oc get secret stackrox-api-token -n stackrox -o go-template --template='{{ "{{" }}.data.token|base64decode}}')" - fi - - echo "========================================================================" - echo " Generating the Cluster Init Bundle" - echo "========================================================================" - echo - - if ! oc get secret admission-control-tls -n stackrox &>/dev/null; then - roxctl -e "$ROX_CENTRAL_ADDRESS" central init-bundles generate local-cluster --output-secrets /tmp/cluster_init_bundle.yaml - oc apply -f /tmp/cluster_init_bundle.yaml -n stackrox - fi - - exit 0 +{{ (.Files.Glob "files/stackrox-init-hook/*").AsConfig | indent 2 }} --- apiVersion: v1 kind: ConfigMap @@ -223,202 +176,7 @@ metadata: 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 +{{ (.Files.Glob "files/stackrox-configure-hook/*").AsConfig | indent 2 }} --- apiVersion: batch/v1 kind: Job diff --git a/infrastructure/templates/fruits-dev.yaml b/infrastructure/templates/fruits-dev.yaml index 4bf5aa2..2393030 100644 --- a/infrastructure/templates/fruits-dev.yaml +++ b/infrastructure/templates/fruits-dev.yaml @@ -73,31 +73,7 @@ metadata: name: cosign-hook namespace: fruits-dev data: - cosign.sh: | - #!/bin/bash - - set -Eeuo pipefail - - mkdir -p /tmp/bin - curl -sfLo /tmp/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.0.2/cosign-linux-amd64 - chmod 755 /tmp/bin/cosign - export PATH="/tmp/bin:$PATH" - - if ! oc get secret code-signature -n fruits-dev &>/dev/null; then - echo "========================================================================" - echo " Generating a keypair" - echo "========================================================================" - echo - - ## 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 +{{ (.Files.Glob "files/cosign-hook/*").AsConfig | indent 2 }} --- apiVersion: batch/v1 kind: Job