diff --git a/.env.traefik.example b/.env.traefik.example new file mode 100644 index 0000000..cdb92cc --- /dev/null +++ b/.env.traefik.example @@ -0,0 +1,48 @@ +# Traefik Configuration +# Copy this file to .env.traefik and configure according to your needs +# +# SERVER_* values are application/setup specific +# TRAEFIK_* values are Traefik configuration values, +# @see https://doc.traefik.io/traefik/reference/install-configuration/boot-environment/#environment-variables + +# If you use an external proxy you can configure the docker network name here +SERVER_FRONTEND_NETWORK=frontend + +# Domain for Traefik dashboard access +# Example: traefik.example.com +SERVER_DOMAIN=traefik.example.com + +# Dashboard Basic Authentication +# Generate with: htpasswd -n admin +# Or use: echo $(htpasswd -n admin) | sed -e s/\\$/\\$\\$/g +# Example: admin:$$apr1$$xyz123... +SERVER_DASHBOARD_AUTH=admin:password + + +######### HTTPS/SSL setup ######### +# This setup can use either Let's Encrypt (default) or local cert/key fiels. This is controlled by setting +# SERVER_CERT_PROVIDER to either "letsencrypt" or "cert-file" + +##### Let's Encrypt ##### +# By default this setup uses Let's Encrypt for https certificates. Alternatively you can provide a local certificate. +SERVER_CERT_PROVIDER=letsencrypt + +# Email for Let's Encrypt notifications +# Example: admin@example.com +TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=admin@example.com + +# ACME (Let's Encrypt) CA Server +# For production (default): https://acme-v02.api.letsencrypt.org/directory +# For staging/testing: https://acme-staging-v02.api.letsencrypt.org/directory +# Staging is recommended for testing to avoid hitting rate limits +# TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory +# For production +TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_CASERVER=https://acme-v02.api.letsencrypt.org/directory + + +##### Local/custom certificae ##### +# SERVER_CERT_PROVIDER=cert-file +# Palce a valid SSL certificate and private key for the domain in the traefik/ssl` directory. +# Then set the following to match the filenames for the provided files. +# SERVER_CUSTOM_CERT_FILE=docker.crt +# SERVER_CUSTOM_KEY_FILE=docker.key diff --git a/.gitignore b/.gitignore index 3f5a37d..124576d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env.local .env.docker.local +.env.traefik diff --git a/README.md b/README.md index 3175434..4216c55 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,18 @@ sudo passwd deploy sudo usermod -aG docker deploy ``` -## Secure Mode Requirement +## Traefik Configuration -This project can only run in secure mode using HTTPS (port 443). To ensure proper functionality, you must provide a valid domain name and an SSL certificate. +### Secure Mode Requirement + +This project can only run in secure mode using HTTPS (port 443). A Traefik reverse proxy will handle HTTPS using either +* Let's encrypt certificates (default) +* Custom certificate/key files ### Steps to Configure Secure Mode: 1. **Domain Name**: Use a fully qualified domain name (FQDN) that resolves to your server's IP address. -2. **SSL Certificate**: Provide a valid SSL certificate and private key for the domain. +2. **SSL Certificate**, either: + - Let traefik generate a certificate using Let's Encrypt (default). - Place the certificate file (`docker.crt`) and the private key file (`docker.key`) in the `traefik/ssl` directory. 3. **Update Configuration**: Ensure the domain name is correctly configured in the `.env.docker.local` file. @@ -60,6 +65,7 @@ Without a valid domain name and SSL certificate, the project will not function a The project uses a `Taskfile.yml` to simplify common operations. Below is a list of the most important tasks you can run: ### Installation and Setup +- **`task traefik_env`**: Configures Traefik to use Let's Encrypt certificates or custom certificates. - **`task install`**: Installs the project, pulls Docker images, sets up the database, and initializes the environment. - **`task reinstall`**: Reinstalls the project from scratch, removing all containers, volumes, and the database. - **`task up`**: Starts the environment without altering the existing state of the containers. diff --git a/Taskfile.yml b/Taskfile.yml index 43fe228..4179996 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -51,6 +51,56 @@ tasks: - task load_templates - task _show_notes + traefik_env: + desc: Setup .env.traefik (domain, email, dashboard auth) + cmds: + - | + if [ ! -f .env.traefik ]; then + echo ".env.traefik does not exist. Copying .env.traefik.example to .env.traefik..." + cp .env.traefik.example .env.traefik + fi + + # Ensure htpasswd is installed + if ! command -v htpasswd >/dev/null 2>&1; then + echo "Error: 'htpasswd' command not found." + echo "Please install it (usually from the 'apache2-utils' or 'httpd-tools' package) and try again." + exit 1 + fi + + echo "" + + echo "Configure Traefik environment" + echo "====================================================" + printf "Enter server domain (e.g. example.com): " + read SERVER_DOMAIN + printf "Enter email for Let's Encrypt (e.g. admin@example.com): " + read TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL + printf "Enter Traefik dashboard/admin username: " + read SERVER_DASHBOARD_USERNAME + printf "Enter Traefik dashboard/admin password: " + read -s SERVER_DASHBOARD_PASSWORD + + # Generate htpasswd entry (username:hash) + HTPASSWD_RAW=$(htpasswd -nb "${SERVER_DASHBOARD_USERNAME}" "${SERVER_DASHBOARD_PASSWORD}") + + # Escape characters that break sed/env ($, /, &) + HTPASSWD_ESCAPED=$(printf '%s\n' "$HTPASSWD_RAW" \ + | sed -e 's/[\/&]/\\&/g' -e 's/\$/\\$/g') + + # Update variables in .env.traefik + sed -i "s/^SERVER_DOMAIN=.*/SERVER_DOMAIN=${SERVER_DOMAIN}/" .env.traefik + sed -i "s/^TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=.*/TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=${TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL}/" .env.traefik + sed -i "s/^SERVER_DASHBOARD_AUTH=.*/SERVER_DASHBOARD_AUTH=${HTPASSWD_ESCAPED}/" .env.traefik + + echo "====================================================" + echo ".env.traefik has been updated." + echo "Domain: ${SERVER_DOMAIN}" + echo "Email: ${TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL}" + echo "User: ${SERVER_DASHBOARD_USERNAME}" + echo "Password: (hidden)" + echo "Auth: (htpasswd line stored in SERVER_DASHBOARD_AUTH)" + echo "====================================================" + reinstall: desc: Reinstall from scratch. Removes the database, all containers, and volumes. deps: @@ -208,6 +258,11 @@ tasks: echo ".env.docker.local does not exist. Copying .env.docker.example to .env.docker.local..." cp .env.docker.example .env.docker.local fi + - | + if [ ! -f .env.traefik ]; then + echo ".env.traefik does not exist. Copying .env.traefik.example to .env.traefik..." + cp .env.traefik.example .env.traefik + fi _dc_compile: deps: diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml index c39fcef..336b71e 100644 --- a/docker-compose.traefik.yml +++ b/docker-compose.traefik.yml @@ -1,26 +1,41 @@ networks: + frontend: + external: true proxy: driver: bridge internal: true services: traefik: - image: traefik:v3.2 + image: traefik:v3.6 container_name: traefik restart: unless-stopped security_opt: - no-new-privileges:true + env_file: + - path: .env.traefik + required: true ports: - "80:80" - "443:443" - - "8080:8080" # Dashboard volumes: - $PWD/traefik/ssl:/certs:ro + - $PWD/traefik/letsencrypt:/letsencrypt - $PWD/traefik/traefik.yml:/traefik.yml:ro - - $PWD/traefik/dynamic-conf.yaml:/config/dynamic-conf.yaml:ro + - $PWD/traefik/dynamic-conf-${SERVER_CERT_PROVIDER:-letsencrypt}.yaml:/config/dynamic-conf.yaml:ro networks: - - frontend + - ${SERVER_FRONTEND_NETWORK:-frontend} - proxy + labels: + - "traefik.enable=true" + # Dashboard authentication + - "traefik.http.middlewares.dashboard-auth.basicauth.users=${SERVER_DASHBOARD_AUTH}" + # Dashboard router + - "traefik.http.routers.dashboard.rule=Host(`${SERVER_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + - "traefik.http.routers.dashboard.entrypoints=websecure" + - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" + - "traefik.http.routers.dashboard.service=api@internal" + - "traefik.http.routers.dashboard.middlewares=dashboard-auth,security-headers@file" socket-proxy: image: itkdev/docker-socket-proxy diff --git a/traefik/dynamic-conf-cert-file.yaml b/traefik/dynamic-conf-cert-file.yaml new file mode 100644 index 0000000..1c519e2 --- /dev/null +++ b/traefik/dynamic-conf-cert-file.yaml @@ -0,0 +1,44 @@ +http: + middlewares: + security-headers: + headers: + frameDeny: true + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 63072000 + customFrameOptionsValue: "SAMEORIGIN" + customResponseHeaders: + X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex" + server: "" + + https-redirect: + redirectScheme: + scheme: https + permanent: true + +tls: + options: + modern: + minVersion: VersionTLS12 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + curvePreferences: + - CurveP521 + - CurveP384 + sniStrict: true + + # Custom certificates (if provided) + # Place your custom certificate files in traefik/ssl/ directory + # Traefik will only use these if the files exist + # If using Let's Encrypt, you can ignore this section or remove the files + certificates: + - certFile: "/certs/{{ env \"SERVER_CUSTOM_CERT_FILE\" }}" + keyFile: "/certs/{{ env \"SERVER_CUSTOM_KEY_FILE\" }}" diff --git a/traefik/dynamic-conf-letsencrypt.yaml b/traefik/dynamic-conf-letsencrypt.yaml new file mode 100644 index 0000000..2cec5c5 --- /dev/null +++ b/traefik/dynamic-conf-letsencrypt.yaml @@ -0,0 +1,36 @@ +http: + middlewares: + security-headers: + headers: + frameDeny: true + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 63072000 + customFrameOptionsValue: "SAMEORIGIN" + customResponseHeaders: + X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex" + server: "" + + https-redirect: + redirectScheme: + scheme: https + permanent: true + +tls: + options: + modern: + minVersion: VersionTLS12 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + curvePreferences: + - CurveP521 + - CurveP384 + sniStrict: true diff --git a/traefik/dynamic-conf.yaml b/traefik/dynamic-conf.yaml deleted file mode 100644 index 63aac3f..0000000 --- a/traefik/dynamic-conf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tls: - certificates: - - certFile: /certs/docker.crt - keyFile: /certs/docker.key - diff --git a/traefik/letsencrypt/.gitignore b/traefik/letsencrypt/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/traefik/letsencrypt/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/traefik/traefik.yml b/traefik/traefik.yml index 46d3370..cd4190d 100644 --- a/traefik/traefik.yml +++ b/traefik/traefik.yml @@ -1,27 +1,56 @@ - api: dashboard: true - insecure: true - debug: true + insecure: false entryPoints: web: address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true + websecure: address: ":443" http: tls: - {} + certResolver: letsencrypt + options: modern@file + http3: {} providers: file: directory: /config + watch: true docker: endpoint: "tcp://socket-proxy:2375" exposedByDefault: false +# Let's Encrypt configuration +certificatesResolvers: + letsencrypt: + acme: + # See .env.traefik.example for more info and stg/prod values + # email: + # caServer: + storage: /letsencrypt/acme.json + httpChallenge: + entryPoint: web + # https://doc.traefik.io/traefik/routing/services/#insecureskipverify serversTransport: insecureSkipVerify: true +# Logging +log: + level: INFO + format: json + +accessLog: + format: json + fields: + headers: + defaultMode: drop