506 lines
15 KiB
YAML
506 lines
15 KiB
YAML
name: Deploy
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
env:
|
|
GIT_HOST: git.jakach.ch
|
|
GIT_REPO: jakach/jakach-login
|
|
GIT_BRANCH: main
|
|
|
|
APP_NAME: auth
|
|
APP_DOMAIN: auth.jakach.ch
|
|
APP_PORT: 447
|
|
|
|
SECURITY_SCAN_ENABLED: ${{ vars.SECURITY_SCAN_ENABLED }}
|
|
CODE_SCAN_ENABLED: ${{ vars.CODE_SCAN_ENABLED }}
|
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
|
TRIVY_IMAGE_SCANNERS: vuln
|
|
TRIVY_VEX: repo
|
|
TRIVY_FS_SCANNERS: vuln,misconfig,secret
|
|
SEMGREP_CONFIG: p/default
|
|
|
|
jobs:
|
|
security_scan:
|
|
runs-on: ubuntu-latest
|
|
|
|
env:
|
|
GIT_USER: ${{ vars.GIT_USER }}
|
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
TRIVY_REGISTRY_USER: ${{ vars.TRIVY_REGISTRY_USER }}
|
|
TRIVY_REGISTRY_PASSWORD: ${{ secrets.TRIVY_REGISTRY_PASSWORD }}
|
|
|
|
steps:
|
|
- name: Scan Docker images for vulnerabilities
|
|
run: |
|
|
set -Eeuo pipefail
|
|
|
|
case "${SECURITY_SCAN_ENABLED:-true}" in
|
|
false|False|FALSE|0|no|No|NO|off|Off|OFF)
|
|
echo "Security scan disabled by SECURITY_SCAN_ENABLED=${SECURITY_SCAN_ENABLED}"
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
: "${GIT_USER:?GIT_USER is required}"
|
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
|
|
|
if command -v apk >/dev/null 2>&1; then
|
|
apk add --no-cache ca-certificates curl git
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update
|
|
apt-get install -y ca-certificates curl git
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
dnf install -y ca-certificates curl git
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
yum install -y ca-certificates curl git
|
|
else
|
|
echo "Unsupported package manager"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v trivy >/dev/null 2>&1; then
|
|
mkdir -p "$HOME/.local/bin"
|
|
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
|
| sh -s -- -b "$HOME/.local/bin"
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
fi
|
|
|
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
|
|
|
git clone \
|
|
--branch "$GIT_BRANCH" \
|
|
"$REPO_URL" \
|
|
source
|
|
|
|
cd source
|
|
|
|
COMPOSE_FILE=""
|
|
|
|
for file in docker-compose.yml compose.yml compose.yaml; do
|
|
if [ -f "$file" ]; then
|
|
COMPOSE_FILE="$file"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$COMPOSE_FILE" ]; then
|
|
echo "No docker compose file found"
|
|
exit 0
|
|
fi
|
|
|
|
if docker compose version >/dev/null 2>&1; then
|
|
docker compose -f "$COMPOSE_FILE" config --images > images.txt
|
|
else
|
|
awk '
|
|
/^[[:space:]]*image:[[:space:]]*/ {
|
|
sub(/^[[:space:]]*image:[[:space:]]*/, "")
|
|
gsub(/["\047]/, "")
|
|
print
|
|
}
|
|
' "$COMPOSE_FILE" > images.txt
|
|
fi
|
|
|
|
sort -u images.txt -o images.txt
|
|
|
|
if [ ! -s images.txt ]; then
|
|
echo "No Docker images found to scan"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "${TRIVY_REGISTRY_USER:-}" ] || [ -n "${TRIVY_REGISTRY_PASSWORD:-}" ]; then
|
|
: "${TRIVY_REGISTRY_USER:?TRIVY_REGISTRY_USER is required when TRIVY_REGISTRY_PASSWORD is set}"
|
|
: "${TRIVY_REGISTRY_PASSWORD:?TRIVY_REGISTRY_PASSWORD is required when TRIVY_REGISTRY_USER is set}"
|
|
echo "Using registry credentials from TRIVY_REGISTRY_USER/TRIVY_REGISTRY_PASSWORD"
|
|
fi
|
|
|
|
TRIVY_IGNORE_ARGS=""
|
|
|
|
if [ -f cve_blacklist.txt ]; then
|
|
awk '
|
|
/^[[:space:]]*($|#)/ {
|
|
next
|
|
}
|
|
{
|
|
print $1
|
|
}
|
|
' cve_blacklist.txt > .trivyignore
|
|
|
|
if [ -s .trivyignore ]; then
|
|
TRIVY_IGNORE_ARGS="--ignorefile .trivyignore"
|
|
echo "Using CVE blacklist from cve_blacklist.txt"
|
|
fi
|
|
fi
|
|
|
|
echo "Scanning Docker images for ${TRIVY_SEVERITY} vulnerabilities:"
|
|
cat images.txt
|
|
|
|
failed=0
|
|
|
|
while IFS= read -r image; do
|
|
[ -n "$image" ] || continue
|
|
|
|
echo ""
|
|
echo "Scanning ${image}"
|
|
|
|
if [ -n "${TRIVY_REGISTRY_USER:-}" ]; then
|
|
if ! trivy \
|
|
image \
|
|
--username "${TRIVY_REGISTRY_USER}" \
|
|
--password "${TRIVY_REGISTRY_PASSWORD}" \
|
|
--scanners "${TRIVY_IMAGE_SCANNERS}" \
|
|
--vex "${TRIVY_VEX}" \
|
|
--exit-code 1 \
|
|
--severity "${TRIVY_SEVERITY}" \
|
|
--ignore-unfixed \
|
|
${TRIVY_IGNORE_ARGS} \
|
|
--no-progress \
|
|
"${image}"; then
|
|
failed=1
|
|
fi
|
|
elif ! trivy \
|
|
image \
|
|
--scanners "${TRIVY_IMAGE_SCANNERS}" \
|
|
--vex "${TRIVY_VEX}" \
|
|
--exit-code 1 \
|
|
--severity "${TRIVY_SEVERITY}" \
|
|
--ignore-unfixed \
|
|
${TRIVY_IGNORE_ARGS} \
|
|
--no-progress \
|
|
"${image}"; then
|
|
failed=1
|
|
fi
|
|
done < images.txt
|
|
|
|
if [ "$failed" -ne 0 ]; then
|
|
echo "WARNING: One or more Docker image scans failed or found ${TRIVY_SEVERITY} vulnerabilities. Deployment stopped."
|
|
exit 1
|
|
fi
|
|
|
|
echo "No high or critical vulnerabilities found"
|
|
|
|
code_scan:
|
|
runs-on: ubuntu-latest
|
|
|
|
env:
|
|
GIT_USER: ${{ vars.GIT_USER }}
|
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
|
|
steps:
|
|
- name: Scan source code
|
|
run: |
|
|
set -Eeuo pipefail
|
|
|
|
case "${CODE_SCAN_ENABLED:-true}" in
|
|
false|False|FALSE|0|no|No|NO|off|Off|OFF)
|
|
echo "Code scan disabled by CODE_SCAN_ENABLED=${CODE_SCAN_ENABLED}"
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
: "${GIT_USER:?GIT_USER is required}"
|
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
|
|
|
if command -v apk >/dev/null 2>&1; then
|
|
apk add --no-cache ca-certificates curl git python3 py3-pip
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update
|
|
apt-get install -y ca-certificates curl git python3 python3-pip python3-venv
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
dnf install -y ca-certificates curl git python3 python3-pip
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
yum install -y ca-certificates curl git python3 python3-pip
|
|
else
|
|
echo "Unsupported package manager"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v semgrep >/dev/null 2>&1; then
|
|
python3 -m venv "$HOME/.semgrep-venv"
|
|
. "$HOME/.semgrep-venv/bin/activate"
|
|
python3 -m pip install --upgrade pip
|
|
python3 -m pip install semgrep
|
|
fi
|
|
|
|
if ! command -v trivy >/dev/null 2>&1; then
|
|
mkdir -p "$HOME/.local/bin"
|
|
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
|
| sh -s -- -b "$HOME/.local/bin"
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
fi
|
|
|
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
|
|
|
git clone \
|
|
--branch "$GIT_BRANCH" \
|
|
"$REPO_URL" \
|
|
source
|
|
|
|
cd source
|
|
|
|
semgrep scan \
|
|
--config "${SEMGREP_CONFIG}" \
|
|
--error \
|
|
--metrics=off
|
|
|
|
trivy fs \
|
|
--scanners "${TRIVY_FS_SCANNERS}" \
|
|
--exit-code 1 \
|
|
--severity "${TRIVY_SEVERITY}" \
|
|
--ignore-unfixed \
|
|
--no-progress \
|
|
.
|
|
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- security_scan
|
|
- code_scan
|
|
|
|
steps:
|
|
- name: Install dependencies
|
|
run: |
|
|
set -e
|
|
|
|
if command -v apk >/dev/null 2>&1; then
|
|
apk add --no-cache openssh-client git bash
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update
|
|
apt-get install -y openssh-client git bash
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
dnf install -y openssh-clients git bash
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
yum install -y openssh-clients git bash
|
|
else
|
|
echo "Unsupported package manager"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Deploy application
|
|
env:
|
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
SSH_USER: ${{ vars.SSH_USER }}
|
|
SSH_IP: ${{ vars.SSH_IP }}
|
|
|
|
GIT_USER: ${{ vars.GIT_USER }}
|
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
|
|
run: |
|
|
cat > deploy.sh <<'OUTER_EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
: "${SSH_KEY:?SSH_KEY is required}"
|
|
: "${SSH_USER:?SSH_USER is required}"
|
|
: "${SSH_IP:?SSH_IP is required}"
|
|
: "${GIT_USER:?GIT_USER is required}"
|
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
|
|
|
REPO_NAME="$(basename "$GIT_REPO")"
|
|
APP_DIR="/srv/systems/${REPO_NAME}"
|
|
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
|
|
printf '%s\n' "$SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key
|
|
chmod 600 ~/.ssh/deploy_key
|
|
|
|
ssh-keyscan -H "$SSH_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
ssh \
|
|
-i ~/.ssh/deploy_key \
|
|
-o StrictHostKeyChecking=yes \
|
|
-o IdentitiesOnly=yes \
|
|
"$SSH_USER@$SSH_IP" \
|
|
"
|
|
export \
|
|
APP_NAME='${APP_NAME}' \
|
|
APP_DOMAIN='${APP_DOMAIN}' \
|
|
APP_PORT='${APP_PORT}' \
|
|
APP_DIR='${APP_DIR}' \
|
|
GIT_HOST='${GIT_HOST}' \
|
|
GIT_REPO='${GIT_REPO}' \
|
|
GIT_BRANCH='${GIT_BRANCH}' \
|
|
GIT_USER='${GIT_USER}' \
|
|
GIT_TOKEN='${GIT_TOKEN}';
|
|
bash -s
|
|
" <<'REMOTE_EOF'
|
|
|
|
set -Eeuo pipefail
|
|
|
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
|
|
|
PROXY_DIR="/srv/systems/proxy"
|
|
NGINX_CONF="${PROXY_DIR}/nginx_conf/nginx.conf"
|
|
GET_CERT_SCRIPT="${PROXY_DIR}/get_cert.sh"
|
|
RENEW_SCRIPT="${PROXY_DIR}/renew_cert.sh"
|
|
FIRST_DEPLOY=0
|
|
PROXY_RESTART_REQUIRED=0
|
|
|
|
# --------------------------------------------------
|
|
# Clone repository if missing
|
|
# --------------------------------------------------
|
|
|
|
if [ ! -d "$APP_DIR/.git" ]; then
|
|
echo "Repository missing, cloning..."
|
|
FIRST_DEPLOY=1
|
|
|
|
mkdir -p "$(dirname "$APP_DIR")"
|
|
|
|
git clone \
|
|
--branch "$GIT_BRANCH" \
|
|
"$REPO_URL" \
|
|
"$APP_DIR"
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# Update repository
|
|
# --------------------------------------------------
|
|
|
|
cd "$APP_DIR"
|
|
|
|
git remote set-url origin "$REPO_URL"
|
|
|
|
git fetch origin "$GIT_BRANCH"
|
|
|
|
git checkout "$GIT_BRANCH"
|
|
|
|
git pull origin "$GIT_BRANCH"
|
|
|
|
# --------------------------------------------------
|
|
# Create nginx reverse proxy entry
|
|
# --------------------------------------------------
|
|
|
|
if [ "$FIRST_DEPLOY" -eq 1 ]; then
|
|
|
|
if ! grep -q "proxy_pass http://192.168.1.109:${APP_PORT}/;" "$NGINX_CONF"; then
|
|
echo "Creating nginx entry..."
|
|
PROXY_RESTART_REQUIRED=1
|
|
|
|
cat >> "$NGINX_CONF" <<NGINXEOF
|
|
|
|
# ---------------------
|
|
# ${APP_NAME} Service
|
|
# ---------------------
|
|
server {
|
|
server_tokens off;
|
|
listen 443 ssl;
|
|
server_name ${APP_DOMAIN};
|
|
|
|
ssl_certificate /etc/nginx/certs/${APP_NAME}.fullchain.pem;
|
|
ssl_certificate_key /etc/nginx/certs/${APP_NAME}.privkey.pem;
|
|
|
|
if (\$allowed_country = no) {
|
|
return 403;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://192.168.1.109:${APP_PORT}/;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
}
|
|
}
|
|
NGINXEOF
|
|
|
|
else
|
|
echo "Nginx entry already exists"
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# Add domain to get_cert.sh
|
|
# --------------------------------------------------
|
|
|
|
if ! grep -q "\"${APP_DOMAIN}\"" "$GET_CERT_SCRIPT"; then
|
|
echo "Adding domain to get_cert.sh"
|
|
|
|
sed -i "/DOMAINS=(/a\ \"${APP_DOMAIN}\"" "$GET_CERT_SCRIPT"
|
|
PROXY_RESTART_REQUIRED=1
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# Create certificate if missing
|
|
# --------------------------------------------------
|
|
|
|
if [ ! -d "/etc/letsencrypt/live/${APP_DOMAIN}" ]; then
|
|
echo "Creating certificate..."
|
|
|
|
sudo certbot certonly \
|
|
--standalone \
|
|
--non-interactive \
|
|
--agree-tos \
|
|
-m admin@jakach.ch \
|
|
-d "${APP_DOMAIN}"
|
|
PROXY_RESTART_REQUIRED=1
|
|
else
|
|
echo "Certificate already exists"
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# Update renew_cert.sh
|
|
# --------------------------------------------------
|
|
|
|
if ! grep -q "${APP_DOMAIN}/privkey.pem" "$RENEW_SCRIPT"; then
|
|
echo "Updating renew_cert.sh"
|
|
|
|
cat >> "$RENEW_SCRIPT" <<CERTEOF
|
|
|
|
cp /etc/letsencrypt/live/${APP_DOMAIN}/privkey.pem certs/${APP_NAME}.privkey.pem
|
|
cp /etc/letsencrypt/live/${APP_DOMAIN}/fullchain.pem certs/${APP_NAME}.fullchain.pem
|
|
|
|
CERTEOF
|
|
PROXY_RESTART_REQUIRED=1
|
|
fi
|
|
|
|
chmod +x "$RENEW_SCRIPT"
|
|
|
|
# --------------------------------------------------
|
|
# Make renew non-interactive
|
|
# --------------------------------------------------
|
|
|
|
sed -i 's/certbot renew$/certbot renew -n/' "$RENEW_SCRIPT" || true
|
|
|
|
# --------------------------------------------------
|
|
# Renew certs + restart proxy
|
|
# --------------------------------------------------
|
|
|
|
if [ "$PROXY_RESTART_REQUIRED" -eq 1 ]; then
|
|
cd "$PROXY_DIR"
|
|
|
|
sudo bash /srv/systems/proxy/renew_cert.sh
|
|
|
|
docker compose down || true
|
|
docker compose up -d --build
|
|
else
|
|
echo "Proxy already configured, skipping certificate renewal and proxy restart"
|
|
fi
|
|
|
|
else
|
|
echo "Existing deployment, skipping proxy setup, certificate renewal and proxy restart"
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# Deploy app
|
|
# --------------------------------------------------
|
|
|
|
cd "$APP_DIR"
|
|
|
|
if [ -f docker-compose.yml ] || [ -f compose.yml ] || [ -f compose.yaml ]; then
|
|
echo "Deploying docker stack..."
|
|
|
|
docker compose down || true
|
|
docker compose up -d --build
|
|
else
|
|
echo "No docker compose file found"
|
|
fi
|
|
|
|
echo "Deployment complete"
|
|
|
|
REMOTE_EOF
|
|
OUTER_EOF
|
|
|
|
chmod +x deploy.sh
|
|
./deploy.sh
|