From 9eb025a21f09feeb1861feace24ec9516546e673 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:14:44 -0600 Subject: [PATCH] Update to production Let's Encrypt certificates --- backend/Dockerfile | 2 +- config/traefik/traefik.yml | 11 +-- docker-compose.yml | 6 ++ docs/CICD-DEPLOY.md | 24 +++++-- docs/PROMPTS.md | 138 +------------------------------------ frontend/Dockerfile | 3 + scripts/inject-secrets.sh | 2 + 7 files changed, 42 insertions(+), 144 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 94f93a3..56c6124 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -25,7 +25,7 @@ RUN npm run build FROM node:20-alpine AS production # Install runtime dependencies only -RUN apk add --no-cache dumb-init +RUN apk add --no-cache dumb-init curl # Set working directory WORKDIR /app diff --git a/config/traefik/traefik.yml b/config/traefik/traefik.yml index 9fdab12..62167fc 100755 --- a/config/traefik/traefik.yml +++ b/config/traefik/traefik.yml @@ -29,10 +29,13 @@ certificatesResolvers: acme: email: admin@motovaultpro.com storage: /data/acme.json - httpChallenge: - entryPoint: web - # Use staging for development - caServer: https://acme-staging-v02.api.letsencrypt.org/directory + dnsChallenge: + provider: cloudflare + delayBeforeCheck: 10 + resolvers: + - "1.1.1.1:53" + - "8.8.8.8:53" + # Production Let's Encrypt (no caServer = production by default) # TLS configuration for local development tls: diff --git a/docker-compose.yml b/docker-compose.yml index 0e46b9a..cdfe42e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: restart: unless-stopped command: - --configFile=/etc/traefik/traefik.yml + environment: + CF_DNS_API_TOKEN_FILE: /run/secrets/cloudflare-dns-token ports: - "80:80" - "443:443" @@ -16,6 +18,7 @@ services: - ./config/traefik/middleware.yml:/etc/traefik/middleware.yml:ro - ./certs:/certs:ro - traefik_data:/data + - ./secrets/app/cloudflare-dns-token.txt:/run/secrets/cloudflare-dns-token:ro networks: frontend: ipv4_address: 10.96.1.50 @@ -73,6 +76,7 @@ services: - "traefik.http.routers.mvp-frontend.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && !PathPrefix(`/api`)" - "traefik.http.routers.mvp-frontend.entrypoints=websecure" - "traefik.http.routers.mvp-frontend.tls=true" + - "traefik.http.routers.mvp-frontend.tls.certresolver=letsencrypt" - "traefik.http.routers.mvp-frontend.priority=10" - "traefik.http.services.mvp-frontend.loadbalancer.server.port=3000" - "traefik.http.services.mvp-frontend.loadbalancer.healthcheck.path=/" @@ -128,11 +132,13 @@ services: - "traefik.http.routers.mvp-backend.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && PathPrefix(`/api`)" - "traefik.http.routers.mvp-backend.entrypoints=websecure" - "traefik.http.routers.mvp-backend.tls=true" + - "traefik.http.routers.mvp-backend.tls.certresolver=letsencrypt" - "traefik.http.routers.mvp-backend.priority=20" # Health check router (bypass auth) - "traefik.http.routers.mvp-backend-health.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && Path(`/api/health`)" - "traefik.http.routers.mvp-backend-health.entrypoints=websecure" - "traefik.http.routers.mvp-backend-health.tls=true" + - "traefik.http.routers.mvp-backend-health.tls.certresolver=letsencrypt" - "traefik.http.routers.mvp-backend-health.priority=30" # Service configuration - "traefik.http.services.mvp-backend.loadbalancer.server.port=3001" diff --git a/docs/CICD-DEPLOY.md b/docs/CICD-DEPLOY.md index a8684bf..b186360 100644 --- a/docs/CICD-DEPLOY.md +++ b/docs/CICD-DEPLOY.md @@ -86,6 +86,7 @@ These variables use GitLab's **File** type, which writes the value to a temporar | `AUTH0_CLIENT_SECRET` | File | Yes | Yes | Auth0 client secret for backend | | `GOOGLE_MAPS_API_KEY` | File | Yes | Yes | Google Maps API key | | `GOOGLE_MAPS_MAP_ID` | File | Yes | No | Google Maps Map ID | +| `CF_DNS_API_TOKEN` | File | Yes | Yes | Cloudflare API token for Let's Encrypt DNS challenge | ### Configuration Variables @@ -97,6 +98,20 @@ These variables use GitLab's **File** type, which writes the value to a temporar Note: `DEPLOY_PATH` is automatically set in `.gitlab-ci.yml` using `GIT_CLONE_PATH` for a stable path. +### Creating Cloudflare API Token + +The `CF_DNS_API_TOKEN` is required for automatic SSL certificate generation via Let's Encrypt DNS-01 challenge. + +1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens) +2. Click **Create Token** +3. Use template: **Edit zone DNS** +4. Configure permissions: + - **Permissions**: Zone > DNS > Edit + - **Zone Resources**: Include > Specific zone > `motovaultpro.com` +5. Click **Continue to summary** then **Create Token** +6. Copy the token value immediately (it won't be shown again) +7. Add as `CF_DNS_API_TOKEN` File variable in GitLab + ### Setting Up a File Type Variable 1. Go to **Settings > CI/CD > Variables** @@ -126,10 +141,11 @@ MotoVaultPro uses a Kubernetes-style secrets pattern where secrets are mounted a ``` secrets/app/ - postgres-password.txt -> /run/secrets/postgres-password - auth0-client-secret.txt -> /run/secrets/auth0-client-secret - google-maps-api-key.txt -> /run/secrets/google-maps-api-key - google-maps-map-id.txt -> /run/secrets/google-maps-map-id + postgres-password.txt -> /run/secrets/postgres-password + auth0-client-secret.txt -> /run/secrets/auth0-client-secret + google-maps-api-key.txt -> /run/secrets/google-maps-api-key + google-maps-map-id.txt -> /run/secrets/google-maps-map-id + cloudflare-dns-token.txt -> /run/secrets/cloudflare-dns-token ``` ### Security Benefits diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index e4c80d0..899afb8 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -23,144 +23,12 @@ Your task is to create a plan that can be dispatched to a seprate set of AI agen You are a senior DevOps SRE in charge of MotoVaultPro. A automotive fleet management application. *** ACTION *** -- There are errors in deployment then also console errors -- It appears when trying to run the npm migrations there is an error. +- The production deployment from GitLab CI is not installing the Let's Encrypt certificates +- You should start looking at if the cloudflare API key is being passed into the pipeline. - Read README.md CLAUDE.md and AI-INDEX.md to understand this code repository in the context of this change. *** CONTEXT *** - This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s. *** CHANGES TO IMPLEMENT *** -- Plan and recommend the best solution for this change - -Here are the relevant logs. -egullickson@mvp-runner-1:~/motovaultpro$ docker compose run --rm mvp-backend npm run migrate -[+] 3/3t 3/31 - ✔ Network motovaultpro_backend Created 0.1s - ✔ Container mvp-postgres Running 0.0s - ✔ Container mvp-redis Running 0.0s -Image motovaultpro-mvp-backend Building -[+] Building 196.2s (26/26) FINISHED - => [internal] load local bake definitions 0.0s - => => reading from stdin 622B -=> => unpacking to docker.io/library/motovaultpro-mvp-backend:latest 9.5s - => resolving provenance for metadata file 0.0s -Image motovaultpro-mvp-backend Built -Container motovaultpro-mvp-backend-run-9a43629523b7 Creating -Container motovaultpro-mvp-backend-run-9a43629523b7 Created -npm error Missing script: "migrate" -npm error -npm error To see a list of scripts, run: -npm error npm run -npm error A complete log of this run can be found in: /home/nodejs/.npm/_logs/2025-12-20T16_15_34_540Z-debug-0.log -index-BZX6X6vG.js:21 [Auth Gate] Module loaded, authInitialized: false -index-BZX6X6vG.js:21 [Navigation] State rehydrated successfully -index-BZX6X6vG.js:21 [Auth0Provider] Component loaded Object -index-BZX6X6vG.js:21 [TokenInjector] Component loaded -index-BZX6X6vG.js:27 [DEBUG App] Render check - isLoading: true isAuthenticated: false isAuthGateReady: false -index-BZX6X6vG.js:27 MotoVaultPro status: Object -index-BZX6X6vG.js:21 [useIsAuthInitialized] Starting poll for auth init -index-BZX6X6vG.js:27 DataSync: Skipping prefetch - user not authenticated -index-BZX6X6vG.js:27 Window width: 1728 User agent mobile: false Mobile mode: false -index-BZX6X6vG.js:21 [Auth Debug] Mobile: false, Loading: true, Authenticated: false, User: null -index-BZX6X6vG.js:21 [Auth Debug] State check: Object -index-BZX6X6vG.js:21 [DEBUG] setAuthInitialized called with: false (was: false ) -index-BZX6X6vG.js:21 [IndexedDB] Loaded 0 items into cache -index-BZX6X6vG.js:21 [IndexedDB] Storage initialized successfully -index-BZX6X6vG.js:21 [useIsAuthInitialized] Poll #1: initialized=false -index-BZX6X6vG.js:21 [useIsAuthInitialized] Poll #2: initialized=false -index-BZX6X6vG.js:21 [useIsAuthInitialized] Poll #3: initialized=false -index-BZX6X6vG.js:21 [Auth0Provider] Redirect callback triggered Object -index-BZX6X6vG.js:21 [Auth0Provider] Component loaded Object -index-BZX6X6vG.js:21 [TokenInjector] Component loaded -index-BZX6X6vG.js:27 [DEBUG App] Render check - isLoading: false isAuthenticated: true isAuthGateReady: false -index-BZX6X6vG.js:27 MotoVaultPro status: Object -index-BZX6X6vG.js:27 [DEBUG App] Auth gate not ready yet, showing loading state -index-BZX6X6vG.js:21 [Auth Debug] Mobile: false, Loading: false, Authenticated: true, User: present -index-BZX6X6vG.js:21 [Auth Debug] State check: Object -index-BZX6X6vG.js:27 [DEBUG App] Render check - isLoading: false isAuthenticated: true isAuthGateReady: false -index-BZX6X6vG.js:27 MotoVaultPro status: Object -index-BZX6X6vG.js:27 [DEBUG App] Auth gate not ready yet, showing loading state -index-BZX6X6vG.js:21 [Auth] IndexedDB storage is ready -index-BZX6X6vG.js:21 [Mobile Auth] Initializing token cache (mobile: false, delay: 100ms) -index-BZX6X6vG.js:21 [useIsAuthInitialized] Poll #4: initialized=false -index-BZX6X6vG.js:21 [Mobile Auth] Token acquired successfully on attempt 1 Object -index-BZX6X6vG.js:21 [Mobile Auth] Token pre-warming successful -index-BZX6X6vG.js:21 [DEBUG] setAuthInitialized called with: true (was: false ) -index-BZX6X6vG.js:21 [DEBUG Auth Gate] Authentication fully initialized -index-BZX6X6vG.js:21 [useIsAuthInitialized] Poll #5: initialized=true -index-BZX6X6vG.js:21 [useIsAuthInitialized] Auth initialized via poll! -index-BZX6X6vG.js:27 [DEBUG App] Render check - isLoading: false isAuthenticated: true isAuthGateReady: true -index-BZX6X6vG.js:27 MotoVaultPro status: Object -index-BZX6X6vG.js:21 [Auth0Provider] Component loaded Object -index-BZX6X6vG.js:21 [TokenInjector] Component loaded -index-BZX6X6vG.js:27 [DEBUG App] Render check - isLoading: false isAuthenticated: true isAuthGateReady: true -index-BZX6X6vG.js:27 MotoVaultPro status: Object -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Query function called - fetching vehicles -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:21 [Mobile Auth] Token acquired successfully on attempt 1 Object -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:27 [useVehicles] Hook called - isAuthenticated: true isLoading: false -index-BZX6X6vG.js:21 Uncaught Error: Minified React error #185; visit https://react.dev/errors/185 for the full message or use the non-minified dev environment for full errors and additional helpful warnings. - at yi (index-BZX6X6vG.js:21:32113) - at hi (index-BZX6X6vG.js:21:31638) - at Yc (index-BZX6X6vG.js:21:64112) - at Hc (index-BZX6X6vG.js:21:63724) - at index-BZX6X6vG.js:27:314956 - at Zu (index-BZX6X6vG.js:21:97033) - at Ad (index-BZX6X6vG.js:21:113396) - at Fd (index-BZX6X6vG.js:21:113280) - at Ad (index-BZX6X6vG.js:21:113441) - at Fd (index-BZX6X6vG.js:21:113280) +- Plan and recommend the best solution for this change \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 9a041d6..b7054fe 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -31,6 +31,9 @@ RUN npm run build # Stage 4: Production stage with nginx FROM nginx:alpine AS production +# Add curl for healthchecks +RUN apk add --no-cache curl + # Create non-root user compatible with nginx RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 -G nginx diff --git a/scripts/inject-secrets.sh b/scripts/inject-secrets.sh index b890211..76999a9 100755 --- a/scripts/inject-secrets.sh +++ b/scripts/inject-secrets.sh @@ -11,6 +11,7 @@ # - AUTH0_CLIENT_SECRET # - GOOGLE_MAPS_API_KEY # - GOOGLE_MAPS_MAP_ID +# - CF_DNS_API_TOKEN (Cloudflare DNS API token for Let's Encrypt certificates) # # Required GitLab CI/CD Variables (Variable type): # - DEPLOY_PATH @@ -70,6 +71,7 @@ inject_secret "POSTGRES_PASSWORD" "postgres-password.txt" || FAILED=1 inject_secret "AUTH0_CLIENT_SECRET" "auth0-client-secret.txt" || FAILED=1 inject_secret "GOOGLE_MAPS_API_KEY" "google-maps-api-key.txt" || FAILED=1 inject_secret "GOOGLE_MAPS_MAP_ID" "google-maps-map-id.txt" || FAILED=1 +inject_secret "CF_DNS_API_TOKEN" "cloudflare-dns-token.txt" || FAILED=1 if [ $FAILED -eq 1 ]; then echo ""