این آزمون چهار درس بخش «reverse proxy و HTTPS» را میسنجد: reverse proxy، WebSocket، HTTPS با Let’s Encrypt و امنیت و هدرهای امنیتی. جواب هر سؤال همان لحظه با توضیح نشان داده میشود و نتیجهات در همین مرورگر ذخیره میشود (پیشرفت من).
اپ پشت Nginx همهی سفارشها را از IP 127.0.0.1 میبیند. چه چیزی کم است؟
اپ فقط اتصال Nginx را میبیند؛ اطلاعات کاربر در هدرها میرسد.
ایمیلهای بازیابی رمز لینکهایی به http://127.0.0.1:3000/reset دارند. علت؟
و برای https، X-Forwarded-Proto هم.
در location /api/، تفاوت proxy_pass http://app; و proxy_pass http://app/; برای درخواست /api/users چیست؟
با مسیر در proxy_pass، بخش جورشدهی location جایگزین میشود.
کاربر هدر X-Forwarded-For: 8.8.8.8 فرستاده. اپ با $proxy_add_x_forwarded_for چه میبیند و به کدام IP اعتماد کند؟
ابتدای زنجیره را کاربر میتواند جعل کند.
اپ خاموش است. کاربر چه کدی میگیرد و در error.log چه میبینی؟
504 برای وقتی است که اپ وصل است ولی دیر جواب میدهد.
گزارشی که ۹۰ ثانیه طول میکشد بعد از ۶۰ ثانیه 504 میگیرد. چه تغییری؟
پیشفرض proxy_read_timeout ۶۰ ثانیه است.
آپلود فایل ۳ مگابایتی 413 میگیرد. چرا؟
Nginx قبل از رسیدن به اپ رد میکند.
برای keepalive بین Nginx و اپ، کدام ترکیب لازم است؟
پیشفرض Nginx به اپ HTTP/1.0 و Connection: close است.
چت WebSocket پشت Nginx با proxy_pass ساده خطای 400 میدهد. علت؟
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
پاسخ موفق دستدادن WebSocket چیست؟
بعد از 101، اتصال تونل دوطرفه میشود.
چت بعد از یک دقیقه سکوت قطع میشود. دو راهحل؟
Nginx اتصال بدون داده را بعد از read_timeout میبندد.
certbot برای چالش HTTP-01 چه چیزی لازم دارد؟
CA به http://دامنه/.well-known/acme-challenge/TOKEN سر میزند.
گواهی Let's Encrypt چند روزه است و تمدید خودکار با چیست؟
تمدید وقتی کمتر از ۳۰ روز مانده.
بلافاصله بعد از گرفتن گواهی چه دستوری میزنی؟
اگر dry-run موفق باشد، تمدید واقعی هم موفق است.
در ssl_certificate کدام فایل certbot را میگذاری؟
بدون زنجیره، بعضی کلاینتها گواهی را تأیید نمیکنند.
چند سایت HTTPS روی یک IP داری. Nginx گواهی درست را از کجا انتخاب میکند؟
هدر Host بعد از برقراری TLS خوانده میشود.
server_tokens off چه میکند؟
بهروز نگه داشتن مهمتر است.
هدرهای امنیتی روی صفحهی 404 نیستند ولی روی 200 هستند. چه چیزی جا افتاده؟
بدون always فقط کدهای موفق و ریدایرکت.
HSTS را با includeSubDomains و max-age یکساله گذاشتهای و intranet.example.com هنوز HTTPS ندارد. چه میشود؟
اول با max-age کوتاه شروع کن و همهی زیردامنهها را HTTPS کن.
با CSP script-src 'self'، چه اتفاقی برای یک <script> درونخطی میافتد؟
اول با Content-Security-Policy-Report-Only آزمایش کن.
در location /admin/ با allow 10.0.0.0/8; deny all; و auth_basic، کاربر از IP مجاز بدون رمز چه کدی میگیرد؟
IP مجاز است، پس نوبت رمز است: 401. از IP غیرمجاز 403.
یک استقرار کامل بساز و با یک اسکریپت ثابت کن:
۱. یک اپ کوچک (Node یا Python) روی 127.0.0.1 با دو بخش: یک API (/api/whoami که IP، Host و پروتکلی را که میبیند برگرداند) و یک WebSocket (/ws/ که پیام را برگرداند: echo).
۲. Nginx با HTTPS برای app.test (گواهی خودامضا کافی است) و ریدایرکت HTTP به HTTPS.
۳. /api/ با هدرهای درست proxy (اپ باید IP واقعی، app.test و https را ببیند) و proxy_read_timeout 10s.
۴. /ws/ با تنظیم درست WebSocket.
۵. هدرهای امنیتی با always و server_tokens off.
۶. اسکریپت آزمایش: ریدایرکت، پاسخ API، دستدادن WebSocket (101)، و وجود HSTS روی ۴۰۴.
راهنمایی
اپ را با Python و کتابخانهی websockets (برای /ws/) یا Node بنویس؛ سادهترین راه، دو پروسهی جدا روی دو پورت (3000 برای API، 8765 برای WebSocket). گواهی: openssl req -x509 ... با subjectAltName=DNS:app.test. هدرهای proxy: include proxy_params;. برای آزمایش با گواهی خودامضا: curl --cacert. برای WebSocket: curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: ...'.
راهحل
pkill -f 'lx-quiz2' 2>/dev/nullrm -rf /srv/lx-quiz2 && mkdir -p /srv/lx-quiz2 /etc/nginx/sslgrep -v '\.test$' /etc/hosts > /tmp/hosts.lx; cat /tmp/hosts.lx > /etc/hostsecho '127.0.0.1 app.test' >> /etc/hostsDEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-websockets > /dev/null 2>&1
cat > /srv/lx-quiz2/lx-quiz2-api.js <<'EOF'// /api/whoami: what the app sees about the clientrequire("http").createServer((req, res) => { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ ip: req.headers["x-real-ip"], host: req.headers["host"], proto: req.headers["x-forwarded-proto"], }) + "\n");}).listen(3000, "127.0.0.1");EOFcat > /srv/lx-quiz2/lx-quiz2-ws.py <<'EOF'# echo WebSocket serverimport asyncio, websocketsasync def echo(ws, path=None): async for msg in ws: await ws.send("echo: " + msg)async def main(): async with websockets.serve(echo, "127.0.0.1", 8765): await asyncio.Future()asyncio.run(main())EOF(exec node /srv/lx-quiz2/lx-quiz2-api.js) > /dev/null 2>&1 &(exec python3 /srv/lx-quiz2/lx-quiz2-ws.py) > /dev/null 2>&1 &
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout /etc/nginx/ssl/app.key -out /etc/nginx/ssl/app.crt \ -subj '/CN=app.test' -addext 'subjectAltName=DNS:app.test' 2>/dev/nullchmod 600 /etc/nginx/ssl/app.keysed -i 's/^\(\s*\)# server_tokens off;/\1server_tokens off;/' /etc/nginx/nginx.conf
cat > /etc/nginx/snippets/lx-quiz2-headers.conf <<'EOF'add_header Strict-Transport-Security "max-age=300" always;add_header X-Content-Type-Options "nosniff" always;add_header X-Frame-Options "SAMEORIGIN" always;add_header Referrer-Policy "strict-origin-when-cross-origin" always;EOFcat > /etc/nginx/sites-available/app.test <<'EOF'server { listen 80; server_name app.test; return 301 https://app.test$request_uri;}server { listen 443 ssl; server_name app.test; ssl_certificate /etc/nginx/ssl/app.crt; ssl_certificate_key /etc/nginx/ssl/app.key; include snippets/lx-quiz2-headers.conf;
location /api/ { proxy_pass http://127.0.0.1:3000; include proxy_params; proxy_read_timeout 10s; } location /ws/ { proxy_pass http://127.0.0.1:8765; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 1h; }}EOFln -sf /etc/nginx/sites-available/app.test /etc/nginx/sites-enabled/sleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1
c() { curl -s --cacert /etc/nginx/ssl/app.crt "$@"; }echo "1) redirect: $(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://app.test/api/whoami)"echo "2) api: $(c --interface 127.0.0.42 https://app.test/api/whoami)"echo "3) ws: $(c -i -m 2 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: SGVsbG8gTG9vcFggMTIzNA==' https://app.test/ws/ | head -n 1 | tr -d '\r')"python3 - <<'EOF'import asyncio, ssl, websocketsasync def main(): ctx = ssl.create_default_context(cafile="/etc/nginx/ssl/app.crt") async with websockets.connect("wss://app.test/ws/", ssl=ctx) as ws: await ws.send("hi") print(" wss echo:", await ws.recv())asyncio.run(main())EOFecho "4) 404 headers: $(c -I https://app.test/nope | grep -ciE '^(strict-transport|x-frame|x-content|referrer)') of 4 security headers, Server: $(c -I https://app.test/nope | grep -i '^server' | tr -d '\r' | cut -d' ' -f2)"nginx: configuration file /etc/nginx/nginx.conf test is successful1) redirect: 301 https://app.test/api/whoami2) api: {"ip":"127.0.0.42","host":"app.test","proto":"https"}3) ws: HTTP/1.1 101 Switching Protocols wss echo: echo: hi4) 404 headers: 4 of 4 security headers, Server: nginxهر قسمت ثابت شد: ریدایرکت با حفظ مسیر؛ اپ IP واقعی (127.0.0.42)، app.test و https را دید؛ دستدادن WebSocket از روی HTTPS با 101 جواب گرفت و یک کلاینت واقعی روی wss:// پیام echo گرفت؛ و صفحهی ۴۰۴ هر چهار هدر امنیتی را دارد و Server بدون نسخه است. (max-age=300 برای آزمایش؛ در سایت واقعی یک سال.)