diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 292ed5b..95ff342 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -20,5 +20,8 @@ jobs: chmod 600 ~/.ssh/id_ed25519 ssh-keyscan 34.90.187.4 >> ~/.ssh/known_hosts + - name: Configure Cloudflare + run: ansible-playbook ansible/cloudflare.yml --vault-password-file <(echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}") + - name: Deploy run: ansible-playbook -i ansible/hosts.ini ansible/deploy-caddy.yml --vault-password-file <(echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}") diff --git a/ansible/cloudflare.yml b/ansible/cloudflare.yml new file mode 100644 index 0000000..1590860 --- /dev/null +++ b/ansible/cloudflare.yml @@ -0,0 +1,173 @@ +--- +- name: Configure Cloudflare for ticket.e-cosplay.fr + hosts: localhost + connection: local + vars_files: + - vault.yml + + vars: + zone_id: "{{ cloudflare_zone_id }}" + cloudflare_record: ticket.e-cosplay.fr + server_ip: 34.90.187.4 + + tasks: + # --- DNS --- + - name: Create or update DNS A record + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records" + method: POST + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + type: A + name: "{{ cloudflare_record }}" + content: "{{ server_ip }}" + ttl: 1 + proxied: true + status_code: [200, 409] + register: dns_result + ignore_errors: true + + - name: Update DNS A record if already exists + when: dns_result.status == 409 or (dns_result.json is defined and not dns_result.json.success) + block: + - name: Get existing DNS record + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records?name={{ cloudflare_record }}&type=A" + method: GET + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + return_content: true + register: existing_dns + + - name: Update DNS record + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/dns_records/{{ existing_dns.json.result[0].id }}" + method: PUT + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + type: A + name: "{{ cloudflare_record }}" + content: "{{ server_ip }}" + ttl: 1 + proxied: true + + # --- SSL/TLS --- + - name: Set SSL mode to Full (Strict) + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/ssl" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: strict + + - name: Enable Always Use HTTPS + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/always_use_https" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: "on" + + - name: Set minimum TLS version to 1.2 + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/min_tls_version" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: "1.2" + + # --- Security headers --- + - name: Enable HSTS + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/security_header" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: + strict_transport_security: + enabled: true + max_age: 31536000 + include_subdomains: true + nosniff: true + + # --- Performance --- + - name: Enable Brotli compression + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/brotli" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: "on" + + - name: Set browser cache TTL to 1 month + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/browser_cache_ttl" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: 2592000 + + # --- Security --- + - name: Set security level to medium + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/settings/security_level" + method: PATCH + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + value: medium + + - name: Enable bot fight mode + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/bot_management" + method: PUT + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + fight_mode: true + ignore_errors: true + + # --- Allow SEO bots --- + - name: Allow Googlebot + uri: + url: "https://api.cloudflare.com/client/v4/zones/{{ zone_id }}/firewall/rules" + method: POST + headers: + Authorization: "Bearer {{ cloudflare_api_token }}" + Content-Type: application/json + body_format: json + body: + - filter: + expression: '(cf.client.bot) or (http.user_agent contains "Googlebot") or (http.user_agent contains "Bingbot") or (http.user_agent contains "bingbot") or (http.user_agent contains "Yandex") or (http.user_agent contains "DuckDuckBot") or (http.user_agent contains "Baiduspider") or (http.user_agent contains "facebookexternalhit") or (http.user_agent contains "Twitterbot") or (http.user_agent contains "LinkedInBot")' + action: allow + description: "Allow SEO and social media bots" + status_code: [200, 409] + ignore_errors: true diff --git a/ansible/vault.yml b/ansible/vault.yml index 7e55328..527a0d3 100644 --- a/ansible/vault.yml +++ b/ansible/vault.yml @@ -1,8 +1,11 @@ $ANSIBLE_VAULT;1.1;AES256 -34376230633964343735383363613430386439326535303762646264333330383166636539643439 -3663303564386133313965343530383761353837626632390a323831366566356234626166646234 -64316232613836376264363237346433393931623863653562656164346534663666373364626130 -3833346535373064660a336234373730383438373233623231363335323162326666346136326162 -65303265386365656164323838666239303639333534626264333962386631323531656262633363 -64333734326466356236633061663933663962646165313935633361356339326366613731613765 -383336626531663034666532636363306130 +64616263316537643530626465343665623830646361623061333265373065353535643435333632 +6639663636363630376437323232633662643430643865630a636431653266353930306231383031 +34393965623762356632633262303632316439333464313161383638366331623833666534653930 +6435656537306566630a333332663632343030643664626261373536393232666262623466643934 +35636534656530623865663737336139386137353738623362393933376563323463346432313562 +36383933306237363936383230303830643030336338353466323933356231343865663663666633 +34386361303033663366356639353933356466333637653436363261613833373664363861633236 +65656162643837633336363836663635626164323763323832396236633131393864363064636463 +61303636346661373362656561356532646364613937663261333939303865326534616237336335 +3737616162653034666634653736393833356331343430653637 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b13cdbf..2b3659b 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -90,6 +90,53 @@ services: - "1025:1025" - "8025:8025" + vault: + image: hashicorp/vault:latest + container_name: e-ticket_vault + cap_add: + - IPC_LOCK + environment: + VAULT_DEV_ROOT_TOKEN_ID: e-ticket + VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 + ports: + - "8200:8200" + volumes: + - vault-data:/vault/file + + minio: + image: minio/minio:latest + container_name: e-ticket_minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: e-ticket + MINIO_ROOT_PASSWORD: e-ticket + ports: + - "9090:9000" + - "9001:9001" + volumes: + - minio-data:/data + + ngrok: + image: ngrok/ngrok:latest + container_name: e-ticket_ngrok + command: http caddy:80 --log stdout + environment: + NGROK_AUTHTOKEN: GXtZtKtRxRF5TFV5pCKD_25f1ALUyQQ9LkyQJgv1dr + ports: + - "4040:4040" + depends_on: + - caddy + + ngrok-sync: + image: curlimages/curl:latest + container_name: e-ticket_ngrok_sync + volumes: + - .:/app + - ./docker/ngrok/sync.sh:/sync.sh + depends_on: + - ngrok + entrypoint: sh /sync.sh + redisinsight: image: redis/redisinsight:latest container_name: e-ticket_redisinsight @@ -103,3 +150,5 @@ volumes: db-data: redis-data: bun-modules: + vault-data: + minio-data: diff --git a/docker/ngrok/sync.sh b/docker/ngrok/sync.sh new file mode 100755 index 0000000..0378a40 --- /dev/null +++ b/docker/ngrok/sync.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +echo "Waiting for ngrok to start..." +sleep 5 + +NGROK_URL="" +RETRIES=10 + +while [ -z "$NGROK_URL" ] && [ "$RETRIES" -gt 0 ]; do + NGROK_URL=$(curl -s http://ngrok:4040/api/tunnels | grep -o '"public_url":"https://[^"]*"' | head -1 | cut -d'"' -f4) + if [ -z "$NGROK_URL" ]; then + echo "Waiting for tunnel..." + sleep 2 + RETRIES=$((RETRIES - 1)) + fi +done + +if [ -z "$NGROK_URL" ]; then + echo "ERROR: Could not get ngrok URL" + exit 1 +fi + +touch /app/.env.local +sed -i '/^OUTSIDE_URL=/d' /app/.env.local +echo "OUTSIDE_URL=$NGROK_URL" >> /app/.env.local + +echo "Ngrok URL: $NGROK_URL" +echo "Written to .env.local" diff --git a/docker/php/dev/Dockerfile b/docker/php/dev/Dockerfile index 0139a1c..d8fe2aa 100644 --- a/docker/php/dev/Dockerfile +++ b/docker/php/dev/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ libicu-dev \ libpng-dev \ libjpeg-dev \ - libfreetype6-dev \ + libfreetype-dev \ libmagickwand-dev \ unzip \ && rm -rf /var/lib/apt/lists/* @@ -19,9 +19,7 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ pdo_pgsql \ pdo_sqlite \ zip \ - xml \ intl \ - mbstring \ gd RUN pecl install redis imagick \ diff --git a/docker/php/prod/Dockerfile b/docker/php/prod/Dockerfile index cf1044b..0400df3 100644 --- a/docker/php/prod/Dockerfile +++ b/docker/php/prod/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ libicu-dev \ libpng-dev \ libjpeg-dev \ - libfreetype6-dev \ + libfreetype-dev \ libmagickwand-dev \ unzip \ && rm -rf /var/lib/apt/lists/* @@ -19,9 +19,7 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ pdo_pgsql \ pdo_sqlite \ zip \ - xml \ intl \ - mbstring \ gd \ opcache