From 8fd3496e05b35fb21824e565e51f88a715a2b25c Mon Sep 17 00:00:00 2001 From: Mohmmed Elfateh Sabry <59346303+elfateh4@users.noreply.github.com> Date: Sun, 10 Aug 2025 23:58:57 +0300 Subject: [PATCH] Add initial configuration files for deployment and services setup --- .env | 26 +++ .github/workflows/deploy.yml | 118 +++++++++++++ authelia/configuration.yml | 152 +++++++++++++++++ crowdsec/acquis.yaml | 4 + docker-compose.yml | 309 +++++++++++++++++++++++++++++++++++ prometheus/prometheus.yml | 16 ++ 6 files changed, 625 insertions(+) create mode 100644 .env create mode 100644 .github/workflows/deploy.yml create mode 100644 authelia/configuration.yml create mode 100644 crowdsec/acquis.yaml create mode 100644 docker-compose.yml create mode 100644 prometheus/prometheus.yml diff --git a/.env b/.env new file mode 100644 index 0000000..a1a6ce2 --- /dev/null +++ b/.env @@ -0,0 +1,26 @@ +# .env + +## Domain / Timezone +DOMAIN=3launchpad.com +TZ=Africa/Cairo + +## ACME (Let's Encrypt) +ACME_EMAIL=admin@3launchpad.com + +## Namecheap DNS API (whitelist your VPS IP in Namecheap API settings) +NAMECHEAP_API_USER=your_namecheap_username +NAMECHEAP_API_KEY=your_namecheap_api_key + +## CrowdSec +# Generate with: docker exec -it crowdsec cscli bouncers add traefik-bouncer +CROWDSEC_BOUNCER_KEY=change_this_long_random_key + +## Umami (PostgreSQL) +UMAMI_DB_USER=umami +UMAMI_DB_PASS=umami_strong_pass +UMAMI_DB_NAME=umami +UMAMI_APP_SECRET=generate_a_random_64char_secret + +## Grafana +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASS=change_me_admin diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..257754d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,118 @@ +# .github/workflows/deploy.yml +name: Deploy Gateway to VPS + +on: + push: + branches: [ "main" ] + paths: + - "docker-compose.yml" + - "authelia/**" + - "crowdsec/**" + - "prometheus/**" + - "grafana/**" + - ".github/workflows/deploy.yml" + workflow_dispatch: + +env: + REMOTE_DIR: ${{ secrets.REMOTE_DIR }} + +concurrency: + group: deploy-prod + cancel-in-progress: true + +jobs: + deploy: + name: Ship to VPS + runs-on: ubuntu-latest + environment: production + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts + + - name: Create target dir + run: | + ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "sudo mkdir -p '${REMOTE_DIR}' && sudo chown -R \$USER:\$USER '${REMOTE_DIR}'" + + - name: Sync repo to VPS (rsync) + run: | + rsync -az --delete \ + -e "ssh -p ${{ secrets.SSH_PORT }}" \ + --exclude ".git" \ + --exclude ".github" \ + ./ "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${REMOTE_DIR}/" + + - name: Write .env on VPS (from GitHub Secrets) + run: | + ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "bash -se" <<'EOF' + set -euo pipefail + cd "${REMOTE_DIR}" + + cat > .env <<'ENVVARS' + # --- Domain / Timezone --- + DOMAIN=${{ secrets.DOMAIN }} + TZ=${{ secrets.TZ }} + + # --- ACME / Let's Encrypt (email only) --- + ACME_EMAIL=${{ secrets.ACME_EMAIL }} + + # --- Namecheap DNS API --- + NAMECHEAP_API_USER=${{ secrets.NAMECHEAP_API_USER }} + NAMECHEAP_API_KEY=${{ secrets.NAMECHEAP_API_KEY }} + + # --- CrowdSec --- + CROWDSEC_BOUNCER_KEY=${{ secrets.CROWDSEC_BOUNCER_KEY }} + + # --- Umami (PostgreSQL) --- + UMAMI_DB_USER=${{ secrets.UMAMI_DB_USER }} + UMAMI_DB_PASS=${{ secrets.UMAMI_DB_PASS }} + UMAMI_DB_NAME=${{ secrets.UMAMI_DB_NAME }} + UMAMI_APP_SECRET=${{ secrets.UMAMI_APP_SECRET }} + + # --- Grafana --- + GRAFANA_ADMIN_USER=${{ secrets.GRAFANA_ADMIN_USER }} + GRAFANA_ADMIN_PASS=${{ secrets.GRAFANA_ADMIN_PASS }} + ENVVARS + EOF + + - name: Pre-flight checks (docker + compose) + run: | + ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" " + docker version && docker compose version + " + + - name: Deploy (pull, up, prune) + run: | + ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "bash -se" <<'EOF' + set -euo pipefail + cd "${REMOTE_DIR}" + + # Warm up networks/volumes and pull images + docker compose pull + + # Bring up (idempotent), remove old orphans + docker compose up -d --remove-orphans + + # Light cleanup of old images (keeps running ones) + docker image prune -af || true + + echo + echo '--- Running containers ---' + docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' + EOF + + - name: Post-deploy smoke checks + run: | + echo "Deployed to ${{ secrets.SSH_HOST }}:${{ secrets.SSH_PORT }} → ${REMOTE_DIR}" + echo "Traefik: https://traefik.gate.${{ secrets.DOMAIN }}" + echo "Portainer: https://portainer.gate.${{ secrets.DOMAIN }}" + echo "Status (Kuma): https://status.gate.${{ secrets.DOMAIN }}" + echo "Grafana: https://grafana.gate.${{ secrets.DOMAIN }}" + echo "Prometheus: https://prometheus.gate.${{ secrets.DOMAIN }}" + echo "Umami: https://umami.gate.${{ secrets.DOMAIN }}" diff --git a/authelia/configuration.yml b/authelia/configuration.yml new file mode 100644 index 0000000..853588c --- /dev/null +++ b/authelia/configuration.yml @@ -0,0 +1,152 @@ +# authelia/configuration.yml +# Authelia v4 configuration for: auth.gate.3launchpad.com +# Behind Traefik (forward-auth), Redis for sessions, SQLite storage. +# ⚠️ Replace all "changeme_*" values or (better) override via env vars: +# AUTHELIA_JWT_SECRET, AUTHELIA_SESSION_SECRET, AUTHELIA_STORAGE_ENCRYPTION_KEY + +######################################################### +# Server & Logging +######################################################### +server: + address: "tcp://0.0.0.0:9091" # Traefik talks to this + buffers: + read: 4096 + write: 4096 +log: + level: info +theme: auto + +# Where to send users if they hit a protected resource without a Referer +default_redirection_url: "https://traefik.gate.3launchpad.com/" + +######################################################### +# Secrets (use env vars in production) +######################################################### +# Prefer setting via Docker env: +# AUTHELIA_JWT_SECRET, AUTHELIA_SESSION_SECRET, AUTHELIA_STORAGE_ENCRYPTION_KEY +jwt_secret: "changeme_jwt_secret" + +######################################################### +# Authentication Backend (local file) +######################################################### +authentication_backend: + file: + path: /config/users_database.yml + # New hashes should be argon2id. Use: `authelia crypto hash generate argon2` + password: + algorithm: argon2id + iterations: 3 + memory: 64 + parallelism: 4 + salt_length: 16 + key_length: 32 + +######################################################### +# Access Control (who can access what) +######################################################### +access_control: + default_policy: deny + + rules: + # Public status page + - domain: "status.gate.3launchpad.com" + policy: bypass + + # Admin-only, require 2FA + - domain: "traefik.gate.3launchpad.com" + subject: ["group:admins"] + policy: two_factor + + - domain: "portainer.gate.3launchpad.com" + subject: ["group:admins"] + policy: two_factor + + # Admin/Devs with 1FA for these tools + - domain_regex: "(grafana|prometheus|umami)\\.gate\\.3launchpad\\.com" + subject: + - "group:admins" + - "group:devs" + policy: one_factor + + # Catch-all for any other subdomain under *.gate.3launchpad.com -> authenticated users + - domain: "*.gate.3launchpad.com" + subject: + - "group:users" + - "group:admins" + - "group:devs" + policy: one_factor + +######################################################### +# Session (cookies + Redis) +######################################################### +session: + name: authelia_session + domain: "gate.3launchpad.com" # cookie scope (covers *.gate.3launchpad.com) + same_site: Lax + expiration: 1h + inactivity: 30m + remember_me_duration: 1M + # secret can be overridden by env AUTHELIA_SESSION_SECRET + secret: "changeme_session_secret" + redis: + host: redis + port: 6379 + # tls: false + +######################################################### +# Regulation (anti-bruteforce) +######################################################### +regulation: + max_retries: 3 + find_time: 2m + ban_time: 10m + +######################################################### +# Storage (SQLite on persistent volume) +######################################################### +storage: + encryption_key: "changeme_storage_key" # override via AUTHELIA_STORAGE_ENCRYPTION_KEY + local: + path: /config/db.sqlite3 + +######################################################### +# Notifier (choose one) +######################################################### +# For testing/dev: writes emails to a file +notifier: + filesystem: + filename: /config/notification.txt + +# For production, comment the block above and use SMTP: +# notifier: +# smtp: +# address: "smtp.gmail.com:587" +# username: "no-reply@3launchpad.com" +# # password via env: AUTHELIA_NOTIFIER_SMTP_PASSWORD +# sender: "3Launchpad Auth " +# subject: "[3Launchpad] {title}" +# startup_check_address: "you@3launchpad.com" +# disable_require_tls: false +# tls: +# server_name: "smtp.gmail.com" +# skip_verify: false + +######################################################### +# TOTP / Duo / WebAuthn (2FA) +######################################################### +totp: + issuer: "3launchpad.com" + period: 30 + skew: 1 + +webauthn: + disable: false + timeout: 60s + display_name: "3Launchpad Gateway" + relying_party_id: "gate.3launchpad.com" + +# If you plan to use Duo Push in the future: +# duo_api: +# hostname: api-XXXXXXXX.duosecurity.com +# integration_key: YOUR_IKEY +# # secret_key via env: AUTHELIA_DUO_API_SECRET_KEY diff --git a/crowdsec/acquis.yaml b/crowdsec/acquis.yaml new file mode 100644 index 0000000..f023943 --- /dev/null +++ b/crowdsec/acquis.yaml @@ -0,0 +1,4 @@ +filenames: + - /var/log/traefik/access.log +labels: + type: traefik diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..69fa60b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,309 @@ +# docker-compose.yml +version: "3.9" + +######################## +# Networks & Volumes +######################## +networks: + traefik_proxy: + name: traefik_proxy + monitoring: + name: monitoring + internal: + name: internal + +volumes: + traefik_letsencrypt: + traefik_logs: + portainer_data: + umami_db_data: + authelia_data: + crowdsec_data: + prometheus_data: + grafana_data: + uptime_kuma_data: + +######################## +# Services +######################## +services: + + ## ───────────────────────────────────────────── + ## Traefik — edge router + ACME (Namecheap DNS) + ## ───────────────────────────────────────────── + traefik: + image: traefik:v3.1 + container_name: traefik + restart: unless-stopped + ports: + - "80:80" + - "443:443" + networks: [traefik_proxy, monitoring] + environment: + # Namecheap DNS challenge auth + NAMECHEAP_API_USER: "${NAMECHEAP_API_USER}" + NAMECHEAP_API_KEY: "${NAMECHEAP_API_KEY}" + TZ: "${TZ}" + command: + # Providers + - --providers.docker=true + - --providers.docker.exposedbydefault=false + + # Entrypoints + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + + # Dashboard/API (internal) + - --api.dashboard=true + + # ACME via DNS-01 (wildcard for *.gate.${DOMAIN}) + - --certificatesresolvers.le.acme.email=${ACME_EMAIL} + - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.le.acme.dnschallenge=true + - --certificatesresolvers.le.acme.dnschallenge.provider=namecheap + # Optional: if DNS propagation is slow, uncomment: + # - --certificatesresolvers.le.acme.dnschallenge.disablepropagationcheck=true + + # Metrics (Prometheus) + - --metrics.prometheus=true + - --metrics.prometheus.addrouterslabels=true + + # Logs + - --accesslog.filepath=/var/log/traefik/access.log + - --accesslog.bufferingsize=100 + - --log.level=INFO + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_letsencrypt:/letsencrypt + - traefik_logs:/var/log/traefik + labels: + - traefik.enable=true + + # Reusable security headers + - traefik.http.middlewares.security-headers.headers.stsSeconds=31536000 + - traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true + - traefik.http.middlewares.security-headers.headers.stsPreload=true + - traefik.http.middlewares.security-headers.headers.browserXssFilter=true + - traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true + - traefik.http.middlewares.security-headers.headers.referrerPolicy=no-referrer-when-downgrade + - traefik.http.middlewares.security-headers.headers.frameDeny=true + + # Authelia ForwardAuth (reusable) + - traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.gate.${DOMAIN} + - traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true + - traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email + + # CrowdSec forward-auth (reusable) + - traefik.http.middlewares.crowdsec.forwardauth.address=http://traefik-bouncer:8080/api/v1/forwardAuth + - traefik.http.middlewares.crowdsec.forwardauth.trustForwardHeader=true + + # Traefik dashboard (protected) + - traefik.http.routers.traefik.rule=Host(`traefik.gate.${DOMAIN}`) + - traefik.http.routers.traefik.entrypoints=websecure + - traefik.http.routers.traefik.tls.certresolver=le + - traefik.http.routers.traefik.service=api@internal + - traefik.http.routers.traefik.middlewares=crowdsec,authelia,security-headers + + ## ───────────────────────────────────────────── + ## Portainer — Docker control plane + ## ───────────────────────────────────────────── + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + restart: unless-stopped + networks: [traefik_proxy] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + labels: + - traefik.enable=true + - traefik.http.routers.portainer.rule=Host(`portainer.gate.${DOMAIN}`) + - traefik.http.routers.portainer.entrypoints=websecure + - traefik.http.routers.portainer.tls.certresolver=le + - traefik.http.routers.portainer.middlewares=crowdsec,authelia,security-headers + - traefik.http.services.portainer.loadbalancer.server.port=9000 + + ## ───────────────────────────────────────────── + ## Umami + PostgreSQL — privacy analytics + ## ───────────────────────────────────────────── + umami-db: + image: postgres:16 + container_name: umami-db + restart: unless-stopped + environment: + POSTGRES_USER: ${UMAMI_DB_USER} + POSTGRES_PASSWORD: ${UMAMI_DB_PASS} + POSTGRES_DB: ${UMAMI_DB_NAME} + TZ: "${TZ}" + volumes: + - umami_db_data:/var/lib/postgresql/data + networks: [internal] + + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + container_name: umami + restart: unless-stopped + depends_on: [umami-db] + environment: + DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASS}@umami-db:5432/${UMAMI_DB_NAME} + APP_SECRET: ${UMAMI_APP_SECRET} + TRACKER_SCRIPT_NAME: umami + TZ: "${TZ}" + networks: [traefik_proxy, internal] + labels: + - traefik.enable=true + - traefik.http.routers.umami.rule=Host(`umami.gate.${DOMAIN}`) + - traefik.http.routers.umami.entrypoints=websecure + - traefik.http.routers.umami.tls.certresolver=le + - traefik.http.routers.umami.middlewares=crowdsec,authelia,security-headers + - traefik.http.services.umami.loadbalancer.server.port=3000 + + ## ───────────────────────────────────────────── + ## Authelia + Redis — SSO/MFA + ## ───────────────────────────────────────────── + authelia: + image: authelia/authelia:latest + container_name: authelia + restart: unless-stopped + depends_on: [redis] + environment: + TZ: "${TZ}" + volumes: + # Provide your config at ./authelia/configuration.yml + - ./authelia/configuration.yml:/config/configuration.yml:ro + - authelia_data:/config + networks: [traefik_proxy, internal] + labels: + - traefik.enable=true + - traefik.http.routers.authelia.rule=Host(`auth.gate.${DOMAIN}`) + - traefik.http.routers.authelia.entrypoints=websecure + - traefik.http.routers.authelia.tls.certresolver=le + - traefik.http.routers.authelia.middlewares=crowdsec,security-headers + - traefik.http.services.authelia.loadbalancer.server.port=9091 + + redis: + image: redis:7-alpine + container_name: authelia-redis + restart: unless-stopped + networks: [internal] + + ## ───────────────────────────────────────────── + ## CrowdSec (LAPI) + Traefik bouncer (forwardAuth) + ## ───────────────────────────────────────────── + crowdsec: + image: crowdsecurity/crowdsec:latest + container_name: crowdsec + restart: unless-stopped + environment: + TZ: "${TZ}" + GID: "0" + COLLECTIONS: "crowdsecurity/traefik crowdsecurity/linux" + volumes: + - crowdsec_data:/var/lib/crowdsec/data + - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro + - traefik_logs:/var/log/traefik:ro + networks: [traefik_proxy] + + # Auto-register the bouncer once (uses CROWDSEC_BOUNCER_KEY from .env) + crowdsec-init: + image: crowdsecurity/crowdsec:latest + container_name: crowdsec-init + depends_on: [crowdsec] + entrypoint: sh -c "cscli bouncers add traefik-bouncer -k ${CROWDSEC_BOUNCER_KEY} || true && sleep 2" + networks: [traefik_proxy] + restart: "no" + + traefik-bouncer: + image: crowdsecurity/traefik-bouncer:latest + container_name: traefik-bouncer + restart: unless-stopped + depends_on: [crowdsec, crowdsec-init] + environment: + CROWDSEC_BOUNCER_API_KEY: "${CROWDSEC_BOUNCER_KEY}" + CROWDSEC_AGENT_HOST: crowdsec:8080 + GIN_MODE: release + networks: [traefik_proxy] + + ## ───────────────────────────────────────────── + ## Uptime Kuma — status page / checks + ## ───────────────────────────────────────────── + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + restart: unless-stopped + volumes: + - uptime_kuma_data:/app/data + networks: [traefik_proxy] + labels: + - traefik.enable=true + - traefik.http.routers.kuma.rule=Host(`status.gate.${DOMAIN}`) + - traefik.http.routers.kuma.entrypoints=websecure + - traefik.http.routers.kuma.tls.certresolver=le + - traefik.http.routers.kuma.middlewares=crowdsec,authelia,security-headers + - traefik.http.services.kuma.loadbalancer.server.port=3001 + + ## ───────────────────────────────────────────── + ## Prometheus + exporters + Grafana + ## ───────────────────────────────────────────── + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + networks: [monitoring, traefik_proxy] + volumes: + - prometheus_data:/prometheus + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + labels: + - traefik.enable=true + - traefik.http.routers.prom.rule=Host(`prometheus.gate.${DOMAIN}`) + - traefik.http.routers.prom.entrypoints=websecure + - traefik.http.routers.prom.tls.certresolver=le + - traefik.http.routers.prom.middlewares=crowdsec,authelia,security-headers + - traefik.http.services.prom.loadbalancer.server.port=9090 + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + restart: unless-stopped + networks: [monitoring] + devices: + - /dev/kmsg:/dev/kmsg + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + restart: unless-stopped + networks: [monitoring] + pid: host + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: ["--path.rootfs=/rootfs"] + + grafana: + image: grafana/grafana-oss:latest + container_name: grafana + restart: unless-stopped + networks: [traefik_proxy, monitoring] + environment: + GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}" + GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASS}" + GF_SERVER_ROOT_URL: https://grafana.gate.${DOMAIN} + TZ: "${TZ}" + volumes: + - grafana_data:/var/lib/grafana + labels: + - traefik.enable=true + - traefik.http.routers.grafana.rule=Host(`grafana.gate.${DOMAIN}`) + - traefik.http.routers.grafana.entrypoints=websecure + - traefik.http.routers.grafana.tls.certresolver=le + - traefik.http.routers.grafana.middlewares=crowdsec,authelia,security-headers + - traefik.http.services.grafana.loadbalancer.server.port=3000 \ No newline at end of file diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..548ad02 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['prometheus:9090'] + - job_name: 'traefik' + metrics_path: /metrics + static_configs: + - targets: ['traefik:8080'] + - job_name: 'cadvisor' + static_configs: + - targets: ['cadvisor:8080'] + - job_name: 'node-exporter' + static_configs: + - targets: ['node-exporter:9100']