masontuckett.xyz

Web Hardening

A Brief Overview of Web Hardening

What is Web Hardening?

Web hardening is the intentional process of enhancing an application’s security posture—by reducing its effective attack surface.

The majority of these enhancements are made at the application layer—defining and permitting how web resources may be interacted with.

In the following sections I will discuss hardening/mitigation strategies: locking down headers, HSTS, SNI traps, key exchange, and a live demo.

Reverse Proxies

Reverse proxies are essential applications for establishing the secure flow of web traffic.

Oftentimes, many reverse proxies are NOT hardened by default—enabling a high ceiling for potential administrative misconfiguration.

It is estimated that only a small fraction of major sites have adopted valid content security policies (CSP).

(Displayed below is a capture demonstrating a major educational institution; there are no hardening artifacts present).

Weber State University Security Score (C-) - No CSP is implemented

Weber State University Security Score (C-) - Almost No Hardening Artifacts

If not mitigated, this essentially opens the door to widespread XSS attacks; this is still not enough, even with a WAF.

The Web

Many corporations and entities are STILL quite slow regarding their adoption of comprehensive web hardening.

Though some configurations may not be enacted due to compatibility concerns:

Insecure fallback ciphers (CBC) are still frequently utilized—set to their respective defaults; opt for secure AEAD ciphers for TLS 1.2+.

Terminology

Key Concepts

DNSSEC Heavily Relies on PKI:

DNSKEY (root public key) signs DS for .xyz.
DNSKEY (tld public key) signs DS for tuckett-test.xyz.
KSK (public key) verified by .xyz’s DS.
ZSK (public key) signed by KSK (private key).
A record (tuckett-test.xyz) signed by ZSK (private key).

Resulting in a robust chain of trust The policy (max-age) is timed, and browsers will not default to HTTP until the policy expires. Take notice of the SHA-384 hash (via OWASP).
1<script src="https://example.com/example-framework.js"
2        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
3        crossorigin="anonymous">
4</script>
Unfortunately, my provider, Let's Encrypt, has dropped support for OCSP—hence stapling's absence in my headers.

Implementation

Use Cases

When hosting your own website, it is incredibly important to scrutinize your threat model.

If you are using an SSG such as Hugo or Jekyll, deep consideration needs to be made about whether the inclusion of scripts or external elements is warranted.

If your goal (as is mine) is to host a simple blog, it is wholly unnecessary to include JS—nonetheless, non-free JS, CDNs, or fonts.

You CANNOT properly audit or trust third-party supply chains, and it is quite frivolous to introduce an avoidable attack surface.

If you are using script tags on an SSG, you defeat its intended purpose and design philosophy.

PLEASE DO NOT IMPLEMENT 'unsafe-inline' OR RAW HTML IN AN SSG!!!

Where Script Tags Are Warranted

If your goal is to showcase a React portfolio or a web app, or you’re utilizing a CMS—then make sure to lock it down!

Simple one-click deployments are fantastic, but you lose the entire benefit of understanding your infrastructure;

In quite the same fashion as the privileged (root) Docker, Portainer, and TrueNAS "enthusiasts."

Prerequisites (DNS)

CAA Records

1### DNS Records ###
2# ! Main CAA Record ! #
3CAA tuckett-test.xyz    3600    128 issue letsencrypt.org
4
5# ! Optional (Barring Wild) ! #
6CAA tuckett-test.xyz    3600    128 issuewild ;

DNSSEC

Refer to your registrar and hosting provider in order to activate these records.

For the majority of users, it is incredibly straightforward, as this process has become highly automated (one-click generation).

Example Records:

1### Enable DNSKEY and DS Record for DNSSEC (USE STRONG ALGORITHMS) ###
2tuckett-test.xyz.	3600	IN	DNSKEY	257 3 13 ggyW7Ek6IYJeQ2vcxwyh3UFieSJ11yn3lKkBoiw6L4bmyu058Nmov0gl 46ZnmTICKQRx4O44N8VHhp0lXb6JEg==
3
4tuckett-test.xyz.	3600	IN	DS	50834 13 2 C668859BAA36112BDD75F60D8AA157DFF99F1A3845CA3431914B4DA1 89A285C8
5tuckett-test.xyz.	3600	IN	DS	50834 13 4 AC7F44830157D8D09B890DD345816C325EE4C663773D114A5836CBA9 29AD0A7EDF7032A082AD9E2D59D0D3938DAEA4A5

Nginx

For this demonstration I will be using my existing setup, but I am currently in the process of migrating from Debian 12 to 13.

For now, this is a bare install (standard binary package), so—unfortunately—I won’t be able to showcase my current transition to rootless Podman.

There are plenty of other fantastic reverse proxies that could be substituted for Nginx; both Caddy and Traefik come to mind.

Package Information

A light selection of packages, as our threat model is largely static.
 1### Standard Nginx Release ###
 2# ! From My Mentee's System ! #
 3smith@smithbarlow:~$ apt-cache policy nginx
 4nginx:
 5  Installed: 1.26.3-3+deb13u1
 6  Candidate: 1.26.3-3+deb13u1
 7  Version table:
 8 *** 1.26.3-3+deb13u1 500
 9        500 https://deb.debian.org/debian trixie/main amd64 Packages
10        500 https://debian.mirror.constant.com trixie/main amd64 Packages
11        100 /var/lib/dpkg/status
12
13### Clear Headers ###
14smith@smithbarlow:/etc/nginx$ apt-cache policy libnginx-mod-http-headers-more-filter
15libnginx-mod-http-headers-more-filter:
16  Installed: 1:0.38-2
17  Candidate: 1:0.38-2
18  Version table:
19 *** 1:0.38-2 500
20        500 https://deb.debian.org/debian trixie/main amd64 Packages
21        500 https://debian.mirror.constant.com trixie/main amd64 Packages
22        100 /var/lib/dpkg/status

I will be sticking with Debian Stableavoiding Nginx’s official Debian repository for stability concerns.

Configuration will still reflect each area of interest—though ideally, I’d entirely avoid standard binary releases and opt for containerization.

If you are running Docker as root (when not necessary), you still open a considerable attack surface—choose Podman (daemonless) as a sane alternative.

"Complexity theater" ignores the glaring inconsistencies with using "extensive" Ansible playbooks to deploy escapable ROOT containers!

Ciphers

You may apply these universally, or across each designated vHost:

 1### /etc/nginx/nginx.conf ###
 2http {
 3# ! Proto Options (TLSv1.2 Optional) ! #
 4ssl_protocols TLSv1.2 TLSv1.3;
 5
 6# ! Manually Restrict Insecure Ciphers (Optional - TLSv1.2) ! #
 7ssl_ciphers ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!DSS;
 8
 9# ! Fine-Tuning (Preferring Server Ciphers is Still Valid) ! #
10# ! Falls Back to secp384r1 ! #
11ssl_ecdh_curve X25519:secp384r1;
12
13# ! Redundant But Forces X25519 FIRST ! #
14ssl_conf_command Groups X25519:P-384;
15
16# ! Optional Stapling ! #
17ssl_stapling on;
18ssl_stapling_verify on;
19
20# ! Optional Resolver ! #
21# ! I'd Advise Against Using Corporate DNS ! #
22resolver 1.1.1.1 8.8.8.8 valid=300s;
23
24# ! Tweak if Needed ! #
25resolver_timeout 10s;
26
27# ! ↓ Nginx Config ↓ ! #
28}

HTTP/2 HTTP/3

HTTP/2:

1### /etc/nginx/sites-enabled/tutorial ###
2server {
3    server_name search.tuckettlab.xyz;
4    listen 443 ssl;
5    listen [::]:443 ssl;
6    http2 on;
7
8# ! ↓ Site Config ↓ ! #
9}

HTTP/3:

 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3listen 443 quic reuseport;
 4listen [::]:443 quic reuseport;
 5
 6# ! HTTP/3 Advertisement ! #
 7add_header Alt-Svc 'h3=":443"; ma=86400' always;
 8
 9# ! ↓ Site Config ↓ ! #
10}

I am personally not going to use HTTP/3; there is no considerable benefit for my use case.
I do not want another open port at this moment.

Security Headers

I prefer using Nginx’s snippets directory to keep my vHost configuration files tidy.

It's as simple as including a file from snippets to your designated vHost.
 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    server_name vhost;
 4    root /var/www/tutorial;
 5    index index.html;
 6
 7    include /etc/nginx/snippets/security-headers.conf;
 8
 9# ! ↓ Site Config ↓ ! #
10}

Security headers can get quite complex, but I assure you that they are incredibly easy to manage.

I will only be able to cover a select amount of implementations—as I don’t want this article to be incessantly exhaustive; Mozilla has a far more granular breakdown available.

Figured below is a basic security configuration—geared towards simple sites.
 1### /etc/nginx/snippets/security-headers.conf ###
 2# ! Basic Static Site Security Configuration ! #
 3# ! Includes Some Redundancy for Demo Purposes ! #
 4
 5### HSTS ###
 6# ! Non-Preloaded Site ! #
 7add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 8
 9# ! Submit to Preload List (hstspreload.org)! #
10# ! Use After Submission ! #
11add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
12
13### CSP ###
14# ! Large List of Variables (Note the Possible External Objects) ! #
15# ! Examples of Linkage: ! #
16# API: script-src 'self' https://subdomain.sld.tld # 
17# Reports: report-uri 'self' https://subdomain.sld.tld/page #
18# Fonts: font-src https://subdomain.sld.tld #
19
20### Content Security Policy ###
21# ! Explicitly Disables Worrisome Vectors ! #
22add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'none'; script-src 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; img-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; font-src 'none'; frame-src 'none'; media-src 'none'; form-action 'none'; manifest-src 'none'; upgrade-insecure-requests" always;
23
24
25### Additional Security Precautions ###
26# ! MIME-Sniffing Prevention (Browser Respects Content-Type Header) ! #
27add_header X-Content-Type-Options "nosniff" always;
28
29### Origin Integrity ###
30# ! Clickjacking Prevention (Legacy) - Blocks iframe Embedding ! #
31add_header X-Frame-Options "DENY" always;
32
33# ! Prevents Non-Origin Side-Channel Attacks ! #
34# ! Completely Breaks Tabnabbing (window.opener or Similar) # !
35add_header Cross-Origin-Opener-Policy "same-origin" always;
36
37# ! Prevents Non-Origin Embeds ! #
38# ! Isolates Content (Requiring CORP or CORS from cross-origin) ! #
39add_header Cross-Origin-Embedder-Policy "require-corp" always;
40
41# ! Blocks Non-Origin From Loading Resources ! #
42# ! Resource Theft/Exfiltration (fetch() or Similar) ! #
43add_header Cross-Origin-Resource-Policy "same-origin" always;
44
45# ! Cross-Origin Isolation ! #
46# ! Isolates Processes (Renders) ! #
47# ! "?0" (Disables) Effectively Groups All Into One Process ! #
48add_header Origin-Agent-Cluster "?1" always;
49
50### Referrer Policy ###
51# ! Does Not Disclose Source (Your Website -> Linked Site) ! #
52# ! Prevents Analytics Bleed (Respects Privacy) ! #
53add_header Referrer-Policy "no-referrer" always;
54
55### Permissions Policy ###
56# ! Important for Dynamic Sites or Services (Some May Be Deprecated, Best to Disable Everything Unnecessary) ! #
57# ! Static Configuration - Note Format: service=() (Blocked) ! #
58# ! Dynamic Configuration - Note Format: service=[](subdomain.sld.tld) (Integrates API into Permissions Policy) ! #
59# ! Included for Demonstration Purposes (Small List): ! #
60
61add_header Permissions-Policy "accelerometer=(), autoplay=(), battery=()" always;

SRI

You will first need to generate a valid hash for your desired assets.

1### Generating the Hash ###
2# ! Opting for SHA-384 ! #
3user@pc:~$ printf 'sha384-%s\n' "$(openssl dgst -sha384 -binary eg.js | openssl base64 -A)"
4sha384-bhFRYtN+I8sKH7+Y95efRQxwXuwViLZyl+42or4SE0+Q1uXOKHMK1Lym2jk2mq7k

Then add your generated hash to any applicable HTML page:

1<!-- crossorigin is Optional - Add if CORS Is Required (Best To Leave Anonymous for Privacy) -->
2<script src="/eg.js"
3        integrity="sha384-bhFRYtN+I8sKH7+Y95efRQxwXuwViLZyl+42or4SE0+Q1uXOKHMK1Lym2jk2mq7k"
4        crossorigin="anonymous">
5</script>

Implementing an SRI check in your reverse proxy goes as follows:

 1### /etc/nginx/sites-enabled/sri-example ###
 2server {
 3    server_name vhost;
 4    
 5    # ! Blocks (Enforces Valid Resources) Scripts/Styles if SRI Check Fails ! #
 6    add_header Integrity-Policy 'blocked-destinations=(script style), endpoints=(integrity)' always;
 7
 8    # ! Report Only Mode ! #
 9    add_header Integrity-Policy-Report-Only 'blocked-destinations=(script style), endpoints=(integrity)' always;
10
11    # ! Specifies Reporting Endpoint (if Needed) ! #
12    add_header Reporting-Endpoints 'integrity="https://tld.sld/report"' always;
13
14    # ! Considering Preloading if Possible (Add "crossorigin=" if Required) ! #
15    # ! Unrelated to HSTS ! #
16    add_header Link '</eg.js>; rel=preload; as=script; integrity="sha384-bhFRYtN..." always;   
17
18# ! ↓ Site Config ↓ ! #
19}

Caching

Caching is incredibly effective in improving response times for HTTP clients.

Serving immutable assets can drastically improve site performance.

 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    # ! Immutable Caching (Matches Common Files) ! #
 4    location ~* \.(css|js|woff2|ico|svg|png|jpg|jpeg|webp)$ {
 5        try_files $uri =404;
 6        add_header Cache-Control "public, max-age=31536000, immutable" always;
 7        expires 1y;
 8    }
 9
10# ! ↓ Site Config ↓ ! #
11}

Method Restriction

It is quite easy to limit the possible HTTP methods that threat actors may utilize for exploitation.

Limiting your request methods ensures that certain types of common attack-related methods (e.g., PUT, DELETE, TRACE) cannot be performed.

It is best to limit your vHost's possible request methods to your intended use case.
1### /etc/nginx/sites-enabled/tutorial ###
2server {
3    location / {
4        limit_except GET HEAD { deny all; }
5        try_files $uri $uri/ =404;
6    }
7
8# ! ↓ Site Config ↓ ! #
9}

UA-Gating

UA-gating is largely trivial, as scanners/attackers frequently spoof user agents; this is not a bulletproof security measure.

 1### /etc/nginx/nginx.conf ###
 2http {
 3    map $http_user_agent $allowed_ua {
 4    # ! Denied by Default ! #
 5        default 0;
 6
 7    # ! Example Chromium Config (Simple Regex >120) ! #
 8        ~*Chrome/[1-9][2-9][0-9]|Chrome/1[2-9][0-9]|Chrome/[2-9][0-9][0-9] 1;
 9    }
10
11# ! ↓ Nginx Config ↓ ! #
12}
13
14### /etc/nginx/sites-enabled/tutorial ###
15server {
16    if ($allowed_ua = 0) { return 403; }
17
18# ! ↓ Site Config ↓ ! #
19}

Rate-Limiting

Rate-limiting is of exceptional importance when serving content over the internet.

Astoundingly, it is somewhat rare to see operators implement this effectively.

1### /etc/nginx/nginx.conf ###
2http {
3# ! 10 MB of Entries - 10 Requests a Second ! #
4# ! You May Also Subtitute r/s for r/m (Minute) ! #
5limit_req_zone  $binary_remote_addr  zone=request_rate_limit:10m  rate=10r/s;
6limit_conn_zone $binary_remote_addr  zone=connections_rate_limit:10m;
7
8# ! ↓ Nginx Config ↓ ! #
9}
 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    # ! Burst of 20 (More Forgiving) ! #
 4    limit_req  zone=request_rate_limit  burst=20  nodelay;
 5    limit_conn connections_rate_limit 20;
 6
 7    # ! Returns 429 (Too Many Requests) ! #
 8    limit_req_status 429;
 9    limit_conn_status 429;
10
11    # ! Optional Logging (Bare Response) ! #
12    error_page 429 = @ratelimit;
13    location @ratelimit {
14    internal;
15    include /etc/nginx/snippets/security-headers.conf;
16    access_log /var/log/nginx/rate_limit.log;
17    default_type text/plain;
18    return 429 "429 - I See You\n";
19    }
20
21# ! ↓ Site Config ↓ ! #
22}
Related, yet VERY tight for my static site:

Buffer/Body Protection:

 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    # ! Overflow Protection (Miniscule Ceilings) ! #
 4    # ! Limiting Methods Will Mitigate Uploads ! #
 5    client_max_body_size 1k;
 6
 7    # ! You Might Need to Bump These Up (2k, 2 4k) ! #
 8    client_header_buffer_size 1k;
 9    large_client_header_buffers 2 1k;
10
11    # ! Proxy Specific (if Needed) ! #
12    proxy_buffer_size 1k;
13    proxy_buffers 2 1k;
14    proxy_busy_buffers_size 1k;
15
16    # ! Specifies How Long Timeouts Will Be ! #
17    client_body_timeout 20s;
18    client_header_timeout 20s;
19    send_timeout 20s;
20
21    # ! Specifies How Many Concurrent HTTP/2 Streams ! #
22    # ! I am More Forgiving Here ! #
23    http2_max_concurrent_streams 48;
24
25# ! ↓ Site Config ↓ ! #
26}

SNI Trap

As modern browsers support SNI by default—it is best to set an SNI “trap” to reduce low-effort scanners and potential fingerprinting.

Optional, but highly recommended; institutes a "feature-lockout" mitigatory strategy
1### /etc/nginx/sites-enabled/default ###
2# ! HTTP #
3server { listen 80 default_server;  listen [::]:80 default_server;  server_name _; return 444; }
4
5# ! HTTPS ! #
6server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name _; ssl_reject_handshake on; }

Another crucial step to reduce trivial spoofing is to implement a simple host sanity check:

 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    listen 443 ssl;
 4    # ! Must Be Hardcoded ! #
 5    server_name sld.tld;
 6
 7    if ($host != "sld.tld") { return 421; }
 8    return 301 https://sld.tld$request_uri;
 9# ! ↓ Site Config ↓ ! #
10}

Privacy Precautions

Just as you may configure your infrastructure to respect its users’ privacy, the infrastructure itself should be as invisible as possible.

Obviously, introducing unnecessary JS and other insipid tracking vectors opens a slew of potential routes for malicious actors—hence the intrinsic case for web minimalism.

Introspect, and reflect honestly on the potential ramifications of perpetuating incessant tracking; how will your own infrastructure backstab you?

Analytics, even for small, static sites—presents nothing but NEGATIVE connotations (for me).

Fingerprinting Mitigation

It is impossible to completely hide your stack and its latent infrastructure.

Although most reverse proxies have optional modules and built-in options for minimizing exposure.

Trixie repositories contain a substantial, mitigatory module: libnginx-mod-http-headers-more-filter.
 1### /etc/nginx/nginx.conf ###
 2http {
 3    # ! DISABLE THIS IMMEDIATELY ! #
 4    # ! DISPLAYS YOUR FULL STACK ! #
 5	server_tokens off;
 6
 7	# ! Strips Additional Server Info ! #
 8	more_clear_headers Server;
 9	more_clear_headers X-Powered-By;
10
11    # ! HTTP Status Codes (Include More if Needed) ! #
12	more_clear_headers -s 200 404 Server;
13
14    # ! Proxy Specific # !
15	proxy_hide_header Server;
16	fastcgi_hide_header X-Powered-By;
17	uwsgi_hide_header Server;
18	scgi_hide_header Server;
19
20    # ! SSL Specific ! #
21	ssl_early_data off;
22	ssl_session_tickets off;
23
24# ! ↓ Nginx Config ↓ ! #
25}
1### /etc/nginx/snippets/error-pages.conf ###
2error_page 404 =404 @e404;
3
4# ! Optional - Link CSP if You're Crazy Like Me ! #
5location @e404 { internal; default_type text/plain; include /etc/nginx/snippets/security-headers.conf; return 404 "404\n"; }
 1### /etc/nginx/sites-enabled/tutorial ###
 2server {
 3    # ! MAKE SURE TO ADD THIS ! #
 4    include /etc/nginx/snippets/error-pages.conf;
 5
 6    # ! Optionally Disable Logs for User Privacy ! #
 7    access_log off;
 8    error_log /var/log/nginx/example-error.log
 9
10# ! ↓ Site Config ↓ ! #
11}

AppArmor

Because this is a bare-install, we’ll need to configure a MAC solution for Nginx.

Apparmor is a fantastic MAC tool, which is quite comparable to SELinux.

Installing and configuring Apparmor is incredibly user-friendly—as the syntax is nearly foolproof.
1### Installing AppArmor ###
2sudo apt update
3sudo apt install -y apparmor apparmor-utils 
4
5### Enable AppArmor ###
6sudo systemctl enable --now apparmor
Now comes the profile!
 1### /etc/apparmor.d/usr.sbin.nginx ###
 2# ! Basic AA Profile ! #
 3# ! Audit & Tweak to Your Needs ! #
 4
 5#include <tunables/global>
 6
 7profile /usr/sbin/nginx flags=(attach_disconnected, mediate_deleted) {
 8
 9  include <abstractions/base>
10  include <abstractions/nameservice>
11  include <abstractions/ssl_certs>
12
13  # ! Caps ! #
14  #  - net_bind_service: bind 80/443
15  #  - set{u,g}id: drop to www-data
16  #  - dac_*: let root master open/rotate logs owned 0640 www-data:adm
17  capability net_bind_service,
18  capability setgid,
19  capability setuid,
20  capability dac_override,
21  capability dac_read_search,
22
23  # ! Networking Settings ! #
24  network inet stream,
25  deny network raw,
26
27  # ! Binary Used ! #
28  /usr/sbin/nginx mr,
29
30  # ! Read-Only Access ! #
31  /etc/nginx/**                 r,
32  /etc/letsencrypt/**           r,
33  /usr/share/nginx/**           r,
34  /etc/mime.types               r,
35  /var/www/**                   r,
36
37  # ! Runtime Stats - PIDs ! #
38  /run/nginx.pid                rw,
39  /run/nginx/**                 rw,
40  /var/lib/nginx/               rw,
41  /var/lib/nginx/**             rwk,
42  /var/cache/nginx/             rw,
43  /var/cache/nginx/**           rwk,
44
45  # ! Logging ! #
46  /var/log/nginx/               rw,
47  /var/log/nginx/**             rwk,
48
49  # ! Denies External/Helper Binaries ! #
50  deny /bin/**                  mrwklx,
51  deny /usr/bin/**              mrwklx,
52}

The profile above should be more than adequate, but it’s best to double-check the permissions and tweak if needed.

Note: I spun this up from a prior config, and tweaked it for compatibility/accessibility concerns.
1### Parsing the Profile ###
2sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
3
4### Enforcing the Profile ###
5sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
6
7### Sanity Check ###
8sudo aa-status
9sudo systemctl restart nginx

Outcomes

You can verify your security standpoint by utilizing a third-party tool such as Mozilla’s HTTP Observatory.

My relevant curl results:
 1### SNI Check ###
 2me@masontuckett:~$ curl -vik https://gnu.org --resolve gnu.org:443:144.202.101.190
 3* Added gnu.org:443:144.202.101.190 to DNS cache
 4* Hostname gnu.org was found in DNS cache
 5*   Trying 144.202.101.190:443...
 6* ALPN: curl offers h2,http/1.1
 7* TLSv1.3 (OUT), TLS handshake, Client hello (1):
 8* TLSv1.3 (IN), TLS alert, unrecognized name (624):
 9* TLS connect error: error:0A000458:SSL routines::tlsv1 unrecognized name
10* closing connection #0
11curl: (35) TLS connect error: error:0A000458:SSL routines::tlsv1 unrecognized name
12
13### Rate-Limiting Check ###
14me@pc:~$ bash -c 'for i in {1..21}; do curl -s -o /dev/null -w "%{http_code}\n" https://masontuckett.xyz & done; wait' | grep -E "200|429" | sort | uniq -c
15     11 200
16     10 429

Closing

I have finally finished this article after a few weeks of complete dormancy—as I am not (typically) the kind of person to lazily publish drafts.

My leisure has become increasingly limited, but I do intend on trimming my own CSP/headers, as there is a lot of residual redundancy.

I DO wish this article was a bit more comprehensive.
I'll likely revisit web hardening—adequately securing more typical, dynamic sites.

It doesn’t impact my security whatsoever—though I vehemently despise the bloat.

I am nearly complete with my migration to rootless Podman—so expect a detailed follow-up with deployable configs soon.

Note: Fail2Ban integration will be covered in a later systems hardening article.

Next Up: Rootless Podman.

You'll see my hatred of Docker/Portainer/Tailscale and JS-laden TrashNAS on full display, haha!

· Mason Tuckett

#web #self-hosting #cybersecurity