Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added
- Webhook proxy security enhancements ([#1398](https://github.com/opendevstack/ods-core/pull/1398/))
- Add client credentials support to external services templates in the ODS API Service ([#1388](https://github.com/opendevstack/ods-core/pull/1388))
- Clean up configuration ([#1404](https://github.com/opendevstack/ods-core/pull/1404))

### Changed
- Adapt webhook proxy to use HMAC ([#1403](https://github.com/opendevstack/ods-core/pull/1403))
Expand Down
62 changes: 32 additions & 30 deletions create-projects/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -243,36 +243,38 @@ podTemplate(
def odsConfigPath = "${env.WORKSPACE}/ods-configuration/ods-core.env"
def scriptsConfigPath = "${env.WORKSPACE}/post-creation-config/${env.POST_SCRIPTS_CONFIG_FILE}"

withCredentials([usernamePassword(credentialsId: "${credentialsId}", passwordVariable: 'password', usernameVariable: 'username')]) {
withEnv([
"PROJECT_ID=${projectId}",
"PROJECT_GROUPS=${projectGroups}",
"PROJECT_ADMIN=${projectAdmins}",
"ODS_NAMESPACE=${odsNamespace}",
"ODS_IMAGE_TAG=${odsImageTag}",
"ODS_GIT_REF=${odsGitRef}",
"ODS_BITBUCKET_PROJECT=${odsBitbucketProject}",
"BITBUCKET_URL=${bitbucketUrl}",
"DOCKER_REGISTRY=${dockerRegistry}",
"CD_USERNAME=${username}",
"CD_PASSWORD=${password}",
"CD_USER_CREDENTIALS_ID=${credentialsId}"
]) {
echo("Environment variables for scripts:")

for (script in scripts) {
def scriptName = script.trim()
def scriptPath = "post-project-creation/${scriptName}"

echo("Executing script: ${scriptName}")

sh(
script: """
"${scriptPath}" "${odsConfigPath}" "${scriptsConfigPath}"
""",
label: "Execute script: ${scriptName}"
)
}
withCredentials([string(credentialsId: "ods-jenkins-webhook-secret", variable: 'WEBHOOK_HMAC_KEY')]) {
withCredentials([usernamePassword(credentialsId: "${credentialsId}", passwordVariable: 'password', usernameVariable: 'username')]) {
withEnv([
"PROJECT_ID=${projectId}",
"PROJECT_GROUPS=${projectGroups}",
"PROJECT_ADMIN=${projectAdmins}",
"ODS_NAMESPACE=${odsNamespace}",
"ODS_IMAGE_TAG=${odsImageTag}",
"ODS_GIT_REF=${odsGitRef}",
"ODS_BITBUCKET_PROJECT=${odsBitbucketProject}",
"BITBUCKET_URL=${bitbucketUrl}",
"DOCKER_REGISTRY=${dockerRegistry}",
"CD_USERNAME=${username}",
"CD_PASSWORD=${password}",
"CD_USER_CREDENTIALS_ID=${credentialsId}"
]) {
echo("Environment variables for scripts:")

for (script in scripts) {
def scriptName = script.trim()
def scriptPath = "post-project-creation/${scriptName}"

echo("Executing script: ${scriptName}")

sh(
script: """
"${scriptPath}" "${odsConfigPath}" "${scriptsConfigPath}"
""",
label: "Execute script: ${scriptName}"
)
}
}
}
}
}
Expand Down
21 changes: 3 additions & 18 deletions jenkins/webhook-proxy/generate_hmac_secret.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,12 @@ set -euo pipefail
# Generates a high-entropy secret suitable for WEBHOOK_HMAC_SECRET.
# Usage:
# bash generate_hmac_secret.sh
# bash generate_hmac_secret.sh 64

length="${1:-64}"

if ! [[ "$length" =~ ^[0-9]+$ ]]; then
echo "error: length must be a positive integer" >&2
exit 1
fi

if (( length < 32 )); then
echo "error: length must be at least 32 characters" >&2
exit 1
fi

if command -v openssl >/dev/null 2>&1; then
secret=$(openssl rand -hex $(((length + 1) / 2)) | cut -c1-"$length")
elif [[ -r /dev/urandom ]]; then
secret=$(tr -dc 'a-f0-9' </dev/urandom | head -c "$length")
secret=$(openssl rand -base64 32)
else
echo "error: neither openssl nor /dev/urandom is available" >&2
echo "error: openssl is not available" >&2
exit 1
fi

printf 'WEBHOOK_HMAC_SECRET=%s\n' "$secret"
printf 'WEBHOOK_HMAC_SECRET=%s\n' "$secret"
2 changes: 1 addition & 1 deletion ods-provisioning-app/ocp-config/Tailorfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ preserve pvc:/metadata/annotations/volume.beta.kubernetes.io/storage-class
preserve-immutable-fields true
ignore-unknown-parameters true

cm,dc,pvc,route,secret,svc,is
pvc,is,cm,secret,dc,svc,route
22 changes: 0 additions & 22 deletions ods-provisioning-app/ocp-config/cm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,12 @@ parameters:
- name: PROV_APP_ATLASSIAN_DOMAIN
required: true
description: the domain of the atlassian toolsuite needed for single signon cookies
- name: PROV_APP_CROWD_PASSWORD
required: true
description: password of the crowd app to authenticate the provision app against
- name: PROV_APP_JASYPT_PASSWORD
required: true
- name: PROV_APP_MAIL_HOST
required: true
description: The hostname of the mailserver
- name: PROV_APP_MAIL_PASSWORD
required: true
description: The password to authenticate against the mail server
- name: PROV_APP_MAIL_USERNAME
required: true
description: The username to authenticate against the mail server
- name: PROV_APP_CROWD_ADMIN_GROUP
required: true
description: The crowd admin group name
- name: PIPELINE_TRIGGER_SECRET
required: true
description: The trigger secret to pass to the webhook proxy
- name: PROV_APP_LOG_LEVEL_ATLASSIAN_CROWD
required: true
description: Log level of Atlassian crowd package
Expand Down Expand Up @@ -232,8 +218,6 @@ objects:
# list of supported webhook events
openshift.jenkins.project.webhookproxy.events=${WEBHOOK_PROXY_EVENTS}

openshift.jenkins.trigger.secret=${PIPELINE_TRIGGER_SECRET}

artifact.group.pattern=${PROV_APP_PACKAGE_PREFIX}.%s

# Cookie Domain
Expand Down Expand Up @@ -261,8 +245,6 @@ objects:

# crowd properties
crowd.local.directory=LocalDirectory
crowd.application.name=provision
crowd.application.password=${PROV_APP_CROWD_PASSWORD}
crowd.server.url=${CROWD_URL}/services/
crowd.cookie.domain=${OPENSHIFT_APPS_BASEDOMAIN}

Expand All @@ -272,8 +254,6 @@ objects:
# local storage
project.storage.local=/opt/provision/history/

jasypt.encryptor.password=${PROV_APP_JASYPT_PASSWORD}

# mail properties
# enable mail sendout with project details
mail.enabled=true
Expand All @@ -288,8 +268,6 @@ objects:
spring.mail.smtps.connectiontimeout=1000
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.auth=true
spring.mail.username=${PROV_APP_MAIL_USERNAME}
spring.mail.password=${PROV_APP_MAIL_PASSWORD}
provison.mail.sender=provision@${PROV_APP_MAIL_HOST}
spring.main.allow-bean-definition-overriding=true

Expand Down
3 changes: 3 additions & 0 deletions ods-provisioning-app/ocp-config/dc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ objects:
memory: ${PROV_APP_MEMORY_REQUEST}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
envFrom:
- secretRef:
name: ods-provisioning-app
volumeMounts:
- mountPath: /opt/provision/history
name: volume-history
Expand Down
32 changes: 32 additions & 0 deletions ods-provisioning-app/ocp-config/secret.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: template.openshift.io/v1
kind: Template
parameters:
- name: PROV_APP_ODS_WEBHOOK_HMAC_SECRET
required: true
- name: PROV_APP_CROWD_PASSWORD
required: true
description: password of the crowd app to authenticate the provision app against
- name: PROV_APP_JASYPT_PASSWORD
required: true
- name: PROV_APP_MAIL_PASSWORD
required: true
description: The password to authenticate against the mail server
- name: PROV_APP_MAIL_USERNAME
required: true
description: The username to authenticate against the mail server
- name: PROV_APP_CROWD_APPLICATION_NAME
value: provision
labels:
app: ods-provisioning-app
objects:
- apiVersion: v1
kind: Secret
metadata:
name: ods-provisioning-app
stringData:
OPENSHIFT_JENKINS_TRIGGER_HMAC_KEY=${PROV_APP_ODS_WEBHOOK_HMAC_SECRET}
CROWD_APPLICATION_NAME=${PROV_APP_CROWD_APPLICATION_NAME}
CROWD_APPLICATION_PASSWORD=${PROV_APP_CROWD_PASSWORD}
JASYPT_ENCRYPTOR_PASSWORD=${PROV_APP_JASYPT_PASSWORD}
SPRING_MAIL_USERNAME=${PROV_APP_MAIL_USERNAME}
SPRING_MAIL_PASSWORD=${PROV_APP_MAIL_PASSWORD}
170 changes: 170 additions & 0 deletions scripts/migrate-provapp-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/bin/bash

set -euo pipefail

CONFIGMAP_NAME="application.properties"
SECRET_NAME="ods-provisioning-app"
DEPLOYMENT_CONFIG="ods-provisioning-app"

usage() {
echo "Usage: $0 -n <namespace>"
exit 1
}

NAMESPACE=""

while [[ $# -gt 0 ]]; do
case "$1" in
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done

if [[ -z "$NAMESPACE" ]]; then
echo "Namespace is required."
usage
fi

# Properties to migrate from ConfigMap to Secret
PROPERTIES=(
"crowd.application.name"
"crowd.application.password"
"jira.admin_user"
"jira.admin_password"
"confluence.admin_user"
"confluence.admin_password"
"bitbucket.admin_user"
"bitbucket.admin_password"
"spring.mail.username"
"spring.mail.password"
"jasypt.encryptor.password"
)

TMP_FILE=$(mktemp)

cleanup() {
rm -f "$TMP_FILE"
}
trap cleanup EXIT

echo "Reading application.properties from ConfigMap ${CONFIGMAP_NAME}..."

oc get configmap "${CONFIGMAP_NAME}" \
-n "${NAMESPACE}" \
-o jsonpath='{.data.properties}' > "${TMP_FILE}"

# Create secret if it does not exist
if ! oc get secret "${SECRET_NAME}" -n "${NAMESPACE}" >/dev/null 2>&1; then
echo "Creating secret ${SECRET_NAME}..."
oc create secret generic "${SECRET_NAME}" \
-n "${NAMESPACE}"
fi

MODIFIED=false

for PROPERTY in "${PROPERTIES[@]}"; do

# Find first non-commented occurrence
LINE=$(grep -E "^[[:space:]]*${PROPERTY}[[:space:]]*=" "${TMP_FILE}" || true)

if [[ -z "${LINE}" ]]; then
echo "Property ${PROPERTY} not found"
continue
fi

VALUE="${LINE#*=}"

# Spring relaxed binding:
# my.property.name -> MY_PROPERTY_NAME
ENV_VAR=$(echo "${PROPERTY}" | tr '[:lower:].-' '[:upper:]__')

echo "Found ${PROPERTY} -> ${ENV_VAR}"

# Check if entry already exists in secret
if oc get secret "${SECRET_NAME}" \
-n "${NAMESPACE}" \
-o jsonpath="{.data.${ENV_VAR}}" 2>/dev/null | grep -q .; then

echo "Secret key ${ENV_VAR} already exists, leaving untouched"

else
echo "Adding ${ENV_VAR} to secret"

PATCH=$(jq -n \
--arg key "$ENV_VAR" \
--arg value "$VALUE" \
'{stringData:{($key):$value}}')

oc patch secret "${SECRET_NAME}" \
-n "${NAMESPACE}" \
--type merge \
-p "$PATCH"
fi

# Remove property from application.properties
sed -i "/^[[:space:]]*${PROPERTY}[[:space:]]*=/d" "${TMP_FILE}"

MODIFIED=true

done

if [[ "${MODIFIED}" == "true" ]]; then
echo "Updating ConfigMap ${CONFIGMAP_NAME}..."

oc create configmap "${CONFIGMAP_NAME}" \
--from-file=properties="${TMP_FILE}" \
-n "${NAMESPACE}" \
--dry-run=client -o yaml | oc apply -f -

echo "ConfigMap updated"
else
echo "No changes required"
fi

echo "Ensuring DeploymentConfig ${DEPLOYMENT_CONFIG} exposes secret values as environment variables..."

if oc get dc "${DEPLOYMENT_CONFIG}" -n "${NAMESPACE}" >/dev/null 2>&1; then

if oc get dc "${DEPLOYMENT_CONFIG}" \
-n "${NAMESPACE}" \
-o jsonpath='{.spec.template.spec.containers[0].envFrom[*].secretRef.name}' \
| grep -qw "${SECRET_NAME}"; then

echo "Secret ${SECRET_NAME} already referenced in DeploymentConfig"

else

oc patch dc "${DEPLOYMENT_CONFIG}" \
-n "${NAMESPACE}" \
--type=json \
-p="$(cat <<EOF
[
{
"op": "add",
"path": "/spec/template/spec/containers/0/envFrom",
"value": [
{
"secretRef": {
"name": "${SECRET_NAME}"
}
}
]
}
]
EOF
)"

echo "DeploymentConfig updated"
fi

else
echo "DeploymentConfig ${DEPLOYMENT_CONFIG} not found"
fi

echo "Done"
Loading
Loading