Harden production security: Dockerfile build, nginx whitelist, and CSP.
Replace Dokploy runtime git clone with Dockerfile build to keep tokens out of the container, add nginx security headers and path whitelist, and scrub templates from the running image after HTML generation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,3 +7,7 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
.venv/
|
.venv/
|
||||||
docker-compose-example/
|
docker-compose-example/
|
||||||
|
docker-compose*.yml
|
||||||
|
dev.sh
|
||||||
|
index.html
|
||||||
|
site.webmanifest
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# Po zmianie wartości uruchom: ./dev.sh config (lub ./dev.sh restart)
|
# Po zmianie wartości uruchom: ./dev.sh config (lub ./dev.sh restart)
|
||||||
|
|
||||||
# --- Frontend Server ---
|
# --- Frontend Server (TYLKO lokalny dev — nie używaj w Dokploy) ---
|
||||||
# Port na hoście (localhost). W kontenerze nginx zawsze nasłuchuje na 80.
|
# Port na hoście (localhost). W kontenerze nginx zawsze nasłuchuje na 80.
|
||||||
FRONTEND_PORT=8082
|
FRONTEND_PORT=8082
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ RUN apk add --no-cache gettext
|
|||||||
# Remove default nginx config
|
# Remove default nginx config
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy nginx config from project root
|
# Copy nginx configs from project root
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy static assets
|
# Copy static assets
|
||||||
|
|||||||
+25
-2
@@ -10,9 +10,24 @@ server {
|
|||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
# Block hidden files and sensitive extensions
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(sh|template|env|yml|yaml|md|git|bak|sql|log)$ {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
# Main page
|
# Main page
|
||||||
location / {
|
location = / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Web app manifest
|
# Web app manifest
|
||||||
@@ -29,7 +44,15 @@ server {
|
|||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Everything else → 404 (no fallback to index.html)
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.youtube.com https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://img.youtube.com data:; frame-src https://www.youtube.com; connect-src 'self' https://www.youtube.com; base-uri 'self'; form-action 'none'; object-src 'none'; upgrade-insecure-requests;" always;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
# Slavic Pulse — Environment Configuration (Dokploy)
|
# Slavic Pulse — Environment Configuration (Dokploy / produkcja)
|
||||||
# Copy to .env before deploy if needed.
|
# Copy to .env before deploy if needed.
|
||||||
# Never commit .env with real credentials to version control.
|
# Never commit .env with real credentials to version control.
|
||||||
|
#
|
||||||
# --- Gitea (opcjonalnie — tylko gdy repo jest prywatne) ---
|
# Tylko zmienne runtime strony — bez GITEA_REPO_URL, bez FRONTEND_PORT.
|
||||||
# Domyślnie clone używa publicznego URL bez auth.
|
# Dokploy buduje obraz z Dockerfile; repo klonowane jest na hoście przez Dokploy.
|
||||||
# Dokploy Gitea Provider (OAuth) NIE autoryzuje git clone wewnątrz kontenera —
|
|
||||||
# nadal potrzebujesz publicznego repo LUB tokena poniżej.
|
|
||||||
# GITEA_REPO_URL=https://oauth2:TWÓJ_TOKEN@gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git
|
|
||||||
|
|
||||||
# --- Strona (meta, SEO, PWA) ---
|
# --- Strona (meta, SEO, PWA) ---
|
||||||
SITE_TITLE="Slavic Pulse"
|
SITE_TITLE="Slavic Pulse"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ site-slavic-pulse-com/
|
|||||||
├── docker-compose.yml # Lokalny dev (build z Dockerfile)
|
├── docker-compose.yml # Lokalny dev (build z Dockerfile)
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── default.conf
|
├── default.conf
|
||||||
|
├── nginx.conf
|
||||||
├── index.html.template # Szablon strony
|
├── index.html.template # Szablon strony
|
||||||
├── generate-from-env.sh # Generator HTML z .env
|
├── generate-from-env.sh # Generator HTML z .env
|
||||||
├── assets/imgs/favico/ # Favicony
|
├── assets/imgs/favico/ # Favicony
|
||||||
@@ -29,19 +30,18 @@ site-slavic-pulse-com/
|
|||||||
- `www.slavic-pulse.com` ← **wymagane** — bez tego www dostaje domyślny cert Traefika (CN=*, self-signed) i telefony pokazują „serwer nie potrafi udowodnić tożsamości”
|
- `www.slavic-pulse.com` ← **wymagane** — bez tego www dostaje domyślny cert Traefika (CN=*, self-signed) i telefony pokazują „serwer nie potrafi udowodnić tożsamości”
|
||||||
4. (Opcjonalnie) **Advanced → Redirects** → preset **www → non-www**, żeby `www` przekierowywało na apex
|
4. (Opcjonalnie) **Advanced → Redirects** → preset **www → non-www**, żeby `www` przekierowywało na apex
|
||||||
5. Traefik routuje ruch HTTPS → port 80 kontenera (bez mapowania portu na hosta)
|
5. Traefik routuje ruch HTTPS → port 80 kontenera (bez mapowania portu na hosta)
|
||||||
6. Deploy
|
6. Deploy — Dokploy klonuje repo na hoście i **buduje obraz z Dockerfile** (brak git clone w kontenerze)
|
||||||
|
|
||||||
**Porty:** nie używaj `ports:` w compose — Traefik/Dokploy łączy się z wewnętrznym `:80`.
|
**Porty:** nie używaj `ports:` w compose — Traefik/Dokploy łączy się z wewnętrznym `:80`.
|
||||||
W `docker ps` kolumna PORTS może być pusta — to normalne przy routingu przez Traefik.
|
W `docker ps` kolumna PORTS może być pusta — to normalne przy routingu przez Traefik.
|
||||||
|
|
||||||
**Gitea Provider w Dokploy** (OAuth) — autoryzuje Dokploy do listowania repo i deployu z UI,
|
**Gitea Provider w Dokploy** (OAuth) — autoryzuje Dokploy do listowania repo i deployu z UI.
|
||||||
ale **nie** zastępuje credentials dla `git clone` wewnątrz kontenera przy starcie.
|
Repo klonowane jest na **hoście** przez Dokploy; kontener nie potrzebuje tokena GITEA ani `git clone` w runtime.
|
||||||
Dla prywatnego repo: ustaw repo jako Public **lub** dodaj `GITEA_REPO_URL` z Deploy Token w `.env`.
|
|
||||||
|
|
||||||
## Jak to działa
|
## Jak to działa
|
||||||
|
|
||||||
1. **Start kontenera** — `git clone` repozytorium z Gitea
|
1. **Build** — Dokploy buduje obraz z `Dockerfile` (nginx + assets + szablony)
|
||||||
2. **`generate-from-env.sh`** — buduje `index.html` i `site.webmanifest` ze zmiennych `.env`
|
2. **Start kontenera** — entrypoint generuje `index.html` i `site.webmanifest` ze zmiennych `.env`, usuwa szablony z dysku
|
||||||
3. **Tło** — miniaturka YouTube (`img.youtube.com`) dopasowana do FHD/vertical
|
3. **Tło** — miniaturka YouTube (`img.youtube.com`) dopasowana do FHD/vertical
|
||||||
4. **Intro wideo** — YouTube IFrame API, autoplay
|
4. **Intro wideo** — YouTube IFrame API, autoplay
|
||||||
5. **Po filmie** — przekierowanie (jeśli `POST_VIDEO_REDIRECT_ENABLED=true`) lub tło z miniaturki
|
5. **Po filmie** — przekierowanie (jeśli `POST_VIDEO_REDIRECT_ENABLED=true`) lub tło z miniaturki
|
||||||
@@ -50,43 +50,61 @@ Dla prywatnego repo: ustaw repo jako Public **lub** dodaj `GITEA_REPO_URL` z Dep
|
|||||||
|
|
||||||
| Zmienna | Opis |
|
| Zmienna | Opis |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| `GITEA_REPO_URL` | Opcjonalnie — URL clone z tokenem dla prywatnego repo |
|
|
||||||
| `VIDEO_ID_FHD` / `VIDEO_ID_VERTICAL` | ID shortów YouTube |
|
| `VIDEO_ID_FHD` / `VIDEO_ID_VERTICAL` | ID shortów YouTube |
|
||||||
| `POST_VIDEO_REDIRECT_URL_*` | URL po zakończeniu filmu |
|
| `POST_VIDEO_REDIRECT_URL_*` | URL po zakończeniu filmu |
|
||||||
| `POST_VIDEO_REDIRECT_ENABLED` | `true` = redirect, `false` = zostaje tło |
|
| `POST_VIDEO_REDIRECT_ENABLED` | `true` = redirect, `false` = zostaje tło |
|
||||||
|
|
||||||
## Rozwiązywanie problemów
|
**Nie dodawaj do produkcyjnego `.env`:** `GITEA_REPO_URL`, `FRONTEND_PORT` — nie są używane w tym modelu deployu.
|
||||||
|
|
||||||
### Kontener `Restarting (128)`, brak portów w `docker ps`
|
## Hardening bezpieczeństwa (wbudowany)
|
||||||
|
|
||||||
**Przyczyna:** `git clone` nie może pobrać repozytorium (najczęściej **prywatne repo** bez credentials).
|
- Build z Dockerfile zamiast runtime git clone — brak tokenów w kontenerze
|
||||||
|
- nginx: whitelist ścieżek, blokada `.env`/`.sh`/`.template`, nagłówki CSP/Referrer-Policy
|
||||||
|
- Szablony i generator usuwane z kontenera po wygenerowaniu HTML
|
||||||
|
- `server_tokens off` — ukryta wersja nginx
|
||||||
|
|
||||||
|
## Weryfikacja po deployu
|
||||||
|
|
||||||
**Diagnostyka na serwerze:**
|
|
||||||
```bash
|
```bash
|
||||||
docker logs slavic-pulse-com-frontend --tail 50
|
# Strona działa
|
||||||
|
curl -sI https://slavic-pulse.com/ | head -5
|
||||||
|
|
||||||
|
# Wrażliwe pliki niedostępne (404)
|
||||||
|
curl -sI https://slavic-pulse.com/.env | grep HTTP
|
||||||
|
curl -sI https://slavic-pulse.com/generate-from-env.sh | grep HTTP
|
||||||
|
curl -sI https://slavic-pulse.com/random-path | grep HTTP
|
||||||
|
|
||||||
|
# Nagłówki bezpieczeństwa
|
||||||
|
curl -sI https://slavic-pulse.com/ | grep -iE 'content-security-policy|referrer-policy|permissions-policy'
|
||||||
|
|
||||||
|
# Brak wersji nginx w Server
|
||||||
|
curl -sI https://slavic-pulse.com/ | grep -i server
|
||||||
|
|
||||||
|
# W kontenerze (na serwerze)
|
||||||
|
docker exec slavic-pulse-com-frontend ls /opt/slavic-pulse 2>&1 # powinno: No such file
|
||||||
|
docker inspect slavic-pulse-com-frontend --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i gitea # brak wyniku
|
||||||
```
|
```
|
||||||
|
|
||||||
Typowy błąd:
|
## Rozwiązywanie problemów
|
||||||
```
|
|
||||||
fatal: could not read Username for 'https://gitea.expert-it.agency-ai.dev': No such device or address
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rozwiązanie A (najprostsze):** ustaw repozytorium jako **Public** w Gitea → Settings → Repository → Make Repository Public → redeploy.
|
|
||||||
|
|
||||||
**Rozwiązanie B (repo prywatne):** w Gitea utwórz Deploy Token / PAT i dodaj do `.env` w Dokploy:
|
|
||||||
```env
|
|
||||||
GITEA_REPO_URL=https://oauth2:TWÓJ_TOKEN@gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Brak portów w `docker ps` (kolumna PORTS pusta)
|
### Brak portów w `docker ps` (kolumna PORTS pusta)
|
||||||
|
|
||||||
Przy Traefik/Dokploy bez `ports:` w compose — **to oczekiwane**.
|
Przy Traefik/Dokploy bez `ports:` w compose — **to oczekiwane**.
|
||||||
Sprawdź w Dokploy UI: domena → Container Port = **80**, kontener `Up (healthy)`.
|
Sprawdź w Dokploy UI: domena → Container Port = **80**, kontener `Up (healthy)`.
|
||||||
|
|
||||||
|
### Kontener nie startuje / unhealthy
|
||||||
|
|
||||||
|
**Diagnostyka na serwerze:**
|
||||||
|
```bash
|
||||||
|
docker logs slavic-pulse-com-frontend --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
Typowe przyczyny: błąd builda Dockerfile, brak `.env`, błąd w `generate-from-env.sh`.
|
||||||
|
|
||||||
### Zmiana konfiguracji po deployu
|
### Zmiana konfiguracji po deployu
|
||||||
|
|
||||||
1. Edytuj `.env` w Dokploy
|
1. Edytuj `.env` w Dokploy
|
||||||
2. Redeploy / restart kontenera (przy starcie ponownie klonuje repo i generuje HTML)
|
2. Redeploy / restart kontenera (entrypoint regeneruje HTML z nowych zmiennych)
|
||||||
|
|
||||||
### Błąd certyfikatu SSL na telefonie („serwer nie potrafi udowodnić…”)
|
### Błąd certyfikatu SSL na telefonie („serwer nie potrafi udowodnić…”)
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
# W UI Dokploy ustaw domenę i Container Port = 80 — Traefik routuje po FQDN.
|
# W UI Dokploy ustaw domenę i Container Port = 80 — Traefik routuje po FQDN.
|
||||||
# Wewnętrznie nginx nasłuchuje na :80 (expose poniżej).
|
# Wewnętrznie nginx nasłuchuje na :80 (expose poniżej).
|
||||||
#
|
#
|
||||||
# Runtime git clone — repozytorium musi być publiczne LUB ustaw GITEA_REPO_URL w .env
|
# Build z Dockerfile — Dokploy klonuje repo na hoście i buduje obraz.
|
||||||
# (patrz README — prywatne repo bez tokenu = exit 128, restart loop).
|
# Brak runtime git clone w kontenerze (hardening bezpieczeństwa).
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
image: nginx:1.27-alpine
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
container_name: slavic-pulse-com-frontend
|
container_name: slavic-pulse-com-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
@@ -21,30 +23,12 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
command: >
|
|
||||||
sh -ce "
|
|
||||||
echo '[slavic-pulse] Installing git + gettext...' &&
|
|
||||||
apk add --no-cache git gettext &&
|
|
||||||
echo '[slavic-pulse] Cloning repo...' &&
|
|
||||||
rm -rf /app &&
|
|
||||||
git clone --depth 1 \"$${GITEA_REPO_URL:-https://gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git}\" /app &&
|
|
||||||
echo '[slavic-pulse] Generating index.html from .env...' &&
|
|
||||||
chmod +x /app/generate-from-env.sh &&
|
|
||||||
TEMPLATE_DIR=/app /app/generate-from-env.sh /usr/share/nginx/html &&
|
|
||||||
echo '[slavic-pulse] Copying assets and nginx config...' &&
|
|
||||||
cp -r /app/assets /usr/share/nginx/html/assets &&
|
|
||||||
rm -f /etc/nginx/conf.d/default.conf &&
|
|
||||||
cp /app/default.conf /etc/nginx/conf.d/default.conf &&
|
|
||||||
echo '[slavic-pulse] Starting nginx...' &&
|
|
||||||
nginx -g 'daemon off;'
|
|
||||||
"
|
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"]
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 15s
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
TEMPLATE_DIR=/opt/slavic-pulse /opt/slavic-pulse/generate-from-env.sh /usr/share/nginx/html
|
TEMPLATE_DIR=/opt/slavic-pulse /opt/slavic-pulse/generate-from-env.sh /usr/share/nginx/html
|
||||||
|
rm -rf /opt/slavic-pulse
|
||||||
|
unset GITEA_REPO_URL FRONTEND_PORT 2>/dev/null || true
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 15;
|
||||||
|
client_body_timeout 10;
|
||||||
|
client_header_timeout 10;
|
||||||
|
send_timeout 10;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user