09.05 Securing the Web Server
SimpleRisk runs behind a web server (Apache or nginx); the web server is the front door. Harden it with TLS 1.2+ only, strong cipher suites, security headers (HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy), rate limiting, and a WAF if your environment warrants. The defaults from a fresh install are usually too permissive for production.
Why this matters
The web server is the public-facing edge of every SimpleRisk install. Every authenticated session, every API call, every form submission goes through it before reaching PHP. A misconfigured web server exposes the application to attacks that the application code can't prevent: TLS downgrade attacks if old protocols are still enabled, cookie hijacking via missing security headers, clickjacking via missing X-Frame-Options, brute-force attacks via missing rate limiting.
SimpleRisk doesn't terminate TLS itself; the web server in front of it does. SimpleRisk doesn't send most security headers itself; the web server adds them. SimpleRisk doesn't enforce rate limits; if rate limiting is needed, the web server provides it. The web server's configuration is therefore the security boundary for everything that reaches the application.
The honest scope to know up front: most of what's in this article is web server configuration, not SimpleRisk configuration. SimpleRisk runs on Apache or nginx; you configure those tools. This article focuses on the configurations relevant to a SimpleRisk deployment; for general web server hardening, the official documentation for your web server is the authoritative source.
Before you start
Have these in hand:
- Root or sudo access to the SimpleRisk server (or to the web server if it's separate, behind a load balancer).
- The web server you're running — Apache, nginx, or other. Examples in this article cover Apache and nginx; adapt for others.
- A valid TLS certificate for your SimpleRisk hostname. Either commercial (purchased) or free via Let's Encrypt — both work; commercial is appropriate for installs with strict procurement requirements; Let's Encrypt is operationally simpler.
- A maintenance window if you're changing TLS configuration in production; users may need to re-authenticate.
- A test client (browser, curl) to verify configuration changes from outside the server.
Step-by-step
1. Use TLS 1.2 or TLS 1.3 only
Old TLS versions (TLS 1.0, TLS 1.1) and SSL (any version) have known vulnerabilities. Disable everything below TLS 1.2.
nginx:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_ecdh_curve secp384r1;
Apache (in your virtual host or ssl.conf):
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLHonorCipherOrder on
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
These configurations are based on Mozilla's "intermediate" recommendation (https://ssl-config.mozilla.org/), which balances modern security with broad client compatibility. If you can constrain to "modern" (TLS 1.3 only, no fallback), even better — but verify your client compatibility first.
Reload the web server after changes; test from outside:
# Test with openssl
openssl s_client -connect your-simplerisk.example.com:443 -tls1_2
# Or use online testers like ssllabs.com
Aim for an A or A+ rating on SSL Labs (https://www.ssllabs.com/ssltest/).
2. Enable HSTS
HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS, preventing TLS-stripping attacks.
nginx:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Apache:
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
If you'd rather not touch the web server configuration, SimpleRisk can send the header for you. Go to Configure > Security, check Enable HTTP Strict Transport Security (HSTS), and set HSTS Max Age (seconds). Check Apply HSTS to all subdomains only if every subdomain is served over HTTPS. The setting is off by default, and the application accepts a max age up to two years. Send the header from one place only, either here or at the web server, so the two don't disagree.
Start small. A browser that has seen the header refuses plain HTTP until the max age runs out, so a broken certificate can lock users out. Try 300 seconds first, confirm HTTPS holds up, then raise it. A max age of 0 tells browsers to forget the policy.
max-age=63072000 is two years (in seconds). includeSubDomains extends to subdomains. preload makes the entry eligible for the browser-preloaded HSTS list (https://hstspreload.org/) — but only enable preload if you're sure every subdomain is HTTPS-ready and you commit to keeping it that way.
3. Enable security headers
A baseline set of HTTP security headers:
nginx:
# Don't let MIME-type sniffing override declared types
add_header X-Content-Type-Options "nosniff" always;
# Don't let the page be framed (clickjacking protection)
add_header X-Frame-Options "SAMEORIGIN" always;
# Send a Referer only to same-origin requests
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Enable XSS protection (legacy header; modern browsers use CSP)
add_header X-XSS-Protection "1; mode=block" always;
# Permissions-Policy (formerly Feature-Policy)
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Apache:
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-XSS-Protection "1; mode=block"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Verify with curl -I https://your-simplerisk.example.com/ — the headers should appear in the response. SimpleRisk itself already sends X-Content-Type-Options, X-Frame-Options, and Referrer-Policy on every page, so the curl output above is checking your web server's copy layered on top of what the application already sends — redundant, but harmless, and worth keeping if you manage multiple applications behind the same web server config.
4. Content Security Policy (CSP)
CSP is the strongest defense against XSS: it tells the browser which origins a page is allowed to load scripts, styles, images, and fonts from, so injected script from an unexpected source has nowhere to run even if it makes it into the page.
Unlike the headers in the previous step, SimpleRisk sends its own CSP from the application rather than leaving it entirely to the web server. New installs ship with it turned on; existing installs can turn it on from Configure > Security, under Enable Content Security Policy. The built-in policy only allows the application's own self-hosted scripts, styles, fonts, and images (plus your configured Base URL), blocks the page from being framed at all, including by SimpleRisk itself, blocks the page's tag from being redirected to another origin, and restricts form submissions to SimpleRisk itself and simplerisk.com (needed for the Extras purchase flow). It still allows inline scripts and styles, since SimpleRisk's UI relies on them throughout — tightening that further would need an application change (nonces or hashes on every inline block), not a configuration one.
If you turn it off because it conflicts with something in your environment — a reverse-proxy path, an injected script from a corporate security agent, a browser extension your users rely on — the previous, wide-open policy takes over so the application keeps functioning while you investigate. Note what broke and consider filing it as a feature request; a policy exception for one integration is usually better than leaving CSP off.
Setting your own Content-Security-Policy header at the web server on top of the built-in one is rarely useful: browsers enforce every Content-Security-Policy header present on a response together, so a resource has to satisfy the strictest reading of all of them combined. In practice that means a web-server policy can only narrow what the built-in one already allows, never broaden it — so add one at the web server only if you specifically want something stricter than the built-in policy, and test in Report-Only mode first:
add_header Content-Security-Policy-Report-Only "default-src 'self'; ...; report-uri /csp-report" always;
The browser sends violation reports without blocking. Review the reports for a week, tune the policy, then switch to enforcing mode.
5. Configure cookie security
The session cookie should have the security flags. SimpleRisk sets HttpOnly by default; verify and add Secure and SameSite.
In simplerisk/includes/config.php (or PHP's session configuration):
ini_set('session.cookie_secure', '1'); // Only over HTTPS
ini_set('session.cookie_httponly', '1'); // Not accessible to JavaScript
ini_set('session.cookie_samesite', 'Lax'); // CSRF protection (or 'Strict' for tighter)
For SimpleRisk installs only accessed over HTTPS (which is what production deployments should be), session.cookie_secure should always be on.
6. Configure rate limiting
SimpleRisk has no native rate limiting. Web-server-level rate limiting protects against:
- Brute-force login attempts.
- API abuse from misbehaving integrations.
- Scraping of public-facing pages.
nginx:
http {
# Define rate-limit zones
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
# Rate-limit login attempts
location ~* ^(/index\.php|/login)$ {
limit_req zone=login_limit burst=5 nodelay;
try_files $uri $uri/ /index.php?$args;
}
# Rate-limit API
location /api/v2/ {
limit_req zone=api_limit burst=20 nodelay;
try_files $uri /api/v2/index.php?$args;
}
}
}
Apache (with mod_evasive or mod_qos):
mod_evasive:
DOSHashTableSize 2048 DOSPageCount 5 DOSSiteCount 100 DOSPageInterval 1 DOSSiteInterval 1 DOSBlockingPeriod 60
Tune to match your legitimate traffic patterns. Too tight and legitimate users get blocked; too loose and brute-force attempts succeed.
7. Restrict access to administrative paths (optional)
Some programs restrict access to administrative paths (/admin/, /configure/) to specific source IPs:
nginx:
location /admin/ {
allow 10.0.0.0/8; # Internal network
allow 192.168.1.100; # Specific admin workstation
deny all;
try_files $uri $uri/ /index.php?$args;
}
This is defense-in-depth — application-level admin permissions still apply, but the network layer prevents external access entirely. Appropriate for installs where admin access from outside specific networks is never legitimate.
8. Hide server software version
Default web server configurations expose the software version in the Server header. Hide it:
nginx:
server_tokens off;
Apache:
ServerTokens Prod
ServerSignature Off
Hiding the version doesn't substantively improve security (attackers fingerprint versions other ways), but it's a small step.
9. Disable unnecessary HTTP methods
SimpleRisk uses GET, POST, PUT, PATCH, DELETE. Methods like TRACE, OPTIONS (for non-CORS use), and CONNECT can be disabled if not needed.
nginx:
if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|HEAD)$) {
return 405;
}
Apache:
Require all denied
10. Configure a Web Application Firewall (WAF) (recommended for production)
A WAF inspects HTTP traffic and blocks known attack patterns. Options:
- ModSecurity with the OWASP Core Rule Set (OWASP CRS) — free, open-source, runs as an Apache or nginx module.
- Cloudflare WAF — managed, easy to configure, requires Cloudflare proxying.
- AWS WAF — managed, integrates with AWS infrastructure.
- Imperva, F5, Akamai — enterprise WAFs with broader feature sets.
A WAF in front of SimpleRisk catches:
- SQL injection attempts (which Phan and SimpleRisk's PDO-based DB layer should already prevent, but defense in depth).
- Cross-site scripting attempts.
- Path traversal attempts.
- Known CVE exploitation patterns.
- High-volume scraping or scanning behavior.
Tune the WAF carefully — overly-aggressive rules block legitimate behavior. Start in observation mode, review false positives, then enforce.
Common pitfalls
A handful of patterns recur with web server hardening.
-
Allowing TLS 1.0 or TLS 1.1 for "compatibility." Both are deprecated and have known vulnerabilities. Modern browsers don't need them; if a client does, that client is the security problem.
-
Setting HSTS without testing first. Once browsers cache the HSTS header, they refuse HTTP for the entire
max-ageperiod. Test in non-production; start with a shortmax-age(e.g., 300 seconds) and increase as you gain confidence. -
Configuring strict CSP without testing. It will break the application in subtle ways. Use Report-Only mode first.
-
Skipping the
Secureflag on session cookies. Allows the cookie to transmit over HTTP, defeating HTTPS-only access patterns. -
Leaving rate limits at the default (none). Brute-force attempts against the login page are continuous on internet-exposed installs. Rate-limit them.
-
Restricting admin paths to "internal IPs" without verifying what "internal" means. A wide internal range may include guest WiFi, vendor-accessible networks, or other untrusted segments. Tight allowlists.
-
Configuring a WAF in enforcing mode without observation period. False positives during a release or workflow change cause user-impacting outages. Observe first, then enforce.
-
Hardening only the web server without the database and application. All three layers matter. See Securing the Database and the rest of this chapter.
-
Forgetting to test from outside the network. Internal testing might not catch issues that only appear from external clients (firewall rules, certificate chains, etc.).
-
Not monitoring web server logs. Logs without review produce no value. Forward to a SIEM or aggregator; alert on suspicious patterns (high 401 rate, scanning paths, etc.).
-
Trusting Cloudflare or your CDN as the only security layer. WAFs can be bypassed; legitimate-looking traffic still hits the origin. The web server should be hardened as if no WAF were in front.
-
Using self-signed certificates in production. Browsers reject them by default; users learn to click through the warning, which trains them to ignore certificate errors generally. Use a real certificate (Let's Encrypt is free).
-
Not redirecting HTTP to HTTPS. A user typing
http://your-simplerisk.example.comshould be redirected tohttps://. Configure the redirect in the web server.