{"slug": "web-security-headers-hsts-csp", "title": "Web security headers + HSTS + CSP", "summary": "The article provides a comprehensive technical guide on implementing web security headers, HSTS, and CSP for managing over 130 production client sites on self-managed Linux infrastructure. It emphasizes that security directly impacts SEO performance, as compromised sites lose rankings, trigger browser warnings, and require months of recovery work. The guide includes specific configuration examples for SSL/TLS certificates via Let's Encrypt, Nginx HTTPS setup, and security header implementation to maintain both user trust and search engine ranking.", "body_md": "Originally published at thatdevpro.com. Part of ThatDevPro's open SEO + AI framework library. ThatDevPro is an SDVOSB-certified veteran-owned web + AI engineering studio. Open-source AI citation toolkit: github.com/Janady13/aio-surfaces.\nHTTPS, Headers, Authentication, WAF, Hardening, Incident Response, and the Security Posture Required to Maintain Search Trust\nA comprehensive reference for web security across the sites Joseph manages. Security is a baseline expectation in 2026 — Google explicitly considers HTTPS a ranking factor (modest), security issues trigger manual actions, browser warnings destroy conversion rates, and breach incidents create permanent brand damage.\nSecurity is often treated as separate from SEO/marketing concerns, but the disciplines significantly overlap. Hacked sites lose rankings, get flagged in browsers as dangerous, drive away users, generate manual actions in GSC, and require months of recovery work. Sites with poor security signals lose trust both with users and with search engines.\nFor Joseph's specific situation managing 130+ production client sites on self-managed Linux infrastructure, security discipline is foundational. A single compromised client site affects the broader hosting environment. WordPress sites particularly attract attack attempts continuously.\nThis framework specifies security implementation across the full stack — server, application, content, and operational dimensions.\nssllabs.com/ssltest/\n— SSL/TLS configuration testingobservatory.mozilla.org\n— security headers analysissecurityheaders.com\n— header analysisssl_certificate_management:\ncertificate_options:\nlets_encrypt:\ncost: \"Free\"\nvalidity: \"90 days (auto-renew)\"\nbest_for: \"Most sites; commodity SSL\"\nautomation: \"Certbot, acme.sh\"\npaid_dv_certificates:\ncost: \"$10-100/year\"\nvalidity: \"1-2 years\"\nbest_for: \"Sites preferring longer validity\"\nbenefit: \"Sometimes faster issuance\"\nextended_validation:\ncost: \"$100-500+/year\"\nvalidity: \"1-2 years\"\nbest_for: \"Financial, e-commerce with high trust requirements\"\nnote: \"Browser address bar treatment reduced; less differentiation than years past\"\nmulti_domain_strategies:\nsan_certificates:\ndescription: \"Multiple domains in single certificate\"\nuse_case: \"Multiple related domains\"\nwildcard_certificates:\ndescription: \"Covers *.example.com subdomains\"\nuse_case: \"Many subdomains\"\nnote: \"Doesn't cover apex; need separate or SAN with apex\"\nmulti_san:\ndescription: \"Multiple unrelated domains in one certificate\"\nuse_case: \"Convenience; fewer certificates to manage\"\nFor Joseph's Debian/Nginx setup managing 130+ sites:\n# Install certbot\napt install certbot python3-certbot-nginx\n# Get certificate for single site\ncertbot --nginx -d example.com -d www.example.com\n# Auto-renewal (typically configured by certbot install)\nsystemctl status certbot.timer\n# Manual renewal test\ncertbot renew --dry-run\nFor mass renewal management:\n# Renew all certificates\ncertbot renew --quiet\n# Should be in cron or systemd timer\n# Default: twice daily check, renews when within 30 days of expiry\nStrong default Nginx HTTPS configuration:\nserver {\nlisten 443 ssl http2;\nlisten [::]:443 ssl http2;\nserver_name example.com www.example.com;\n# SSL Certificate\nssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;\nssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;\n# Modern SSL Configuration\nssl_protocols TLSv1.2 TLSv1.3;\nssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';\nssl_prefer_server_ciphers off;\nssl_session_cache shared:SSL:10m;\nssl_session_timeout 10m;\n# OCSP Stapling\nssl_stapling on;\nssl_stapling_verify on;\nssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;\nresolver 1.1.1.1 8.8.8.8 valid=300s;\n# Security Headers (see Section 3)\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n# ... rest of configuration\n}\n# Redirect HTTP to HTTPS\nserver {\nlisten 80;\nlisten [::]:80;\nserver_name example.com www.example.com;\nreturn 301 https://$host$request_uri;\n}\nAfter implementation, verify with SSL Labs (ssllabs.com/ssltest/\n):\nSecurity headers add layers of browser-enforced protection.\n# Strict-Transport-Security (HSTS)\n# Forces HTTPS, prevents downgrade attacks\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n# X-Frame-Options\n# Prevents clickjacking via iframe embedding\nadd_header X-Frame-Options \"SAMEORIGIN\" always;\n# X-Content-Type-Options\n# Prevents MIME sniffing\nadd_header X-Content-Type-Options \"nosniff\" always;\n# Referrer-Policy\n# Controls referrer information sent\nadd_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n# Permissions-Policy (formerly Feature-Policy)\n# Controls browser features\nadd_header Permissions-Policy \"geolocation=(), microphone=(), camera=()\" always;\n# Content-Security-Policy (most complex; see below)\nadd_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self';\" always;\n# Cross-Origin policies\nadd_header Cross-Origin-Opener-Policy \"same-origin\" always;\nadd_header Cross-Origin-Resource-Policy \"same-origin\" always;\nadd_header Cross-Origin-Embedder-Policy \"require-corp\" always;\nCSP is the most complex but most powerful header. It defines what sources of scripts, styles, images, etc. are allowed:\ncsp_directives:\ndefault_src:\npurpose: \"Fallback for other directives\"\ntypical: \"'self'\"\nscript_src:\npurpose: \"Allowed JavaScript sources\"\ncommon_additions:\n- \"'self'\"\n- \"https://www.googletagmanager.com\"\n- \"https://www.google-analytics.com\"\n- \"'unsafe-inline'\" # Often required; reduces protection\navoid: \"'unsafe-eval' unless absolutely required\"\nstyle_src:\npurpose: \"Allowed CSS sources\"\ncommon: \"'self' 'unsafe-inline' https://fonts.googleapis.com\"\nimg_src:\npurpose: \"Allowed image sources\"\ncommon: \"'self' data: https:\"\nfont_src:\npurpose: \"Allowed font sources\"\ncommon: \"'self' https://fonts.gstatic.com\"\nconnect_src:\npurpose: \"Allowed fetch/XHR destinations\"\ncommon: \"'self' https://www.google-analytics.com\"\nframe_ancestors:\npurpose: \"Who can iframe this page\"\ntypical: \"'self'\"\nnote: \"Replaces X-Frame-Options\"\nbase_uri:\npurpose: \"Restrict <base> element\"\ntypical: \"'self'\"\nform_action:\npurpose: \"Where forms can submit\"\ntypical: \"'self'\"\ncsp_implementation:\nstep_1_audit:\n- Inventory all script sources\n- Inventory all style sources\n- Inventory all image, font, fetch sources\nstep_2_report_only:\nheader: \"Content-Security-Policy-Report-Only\"\npurpose: \"Test policy without enforcement\"\nduration: \"Run 1-2 weeks; collect violations\"\nstep_3_iterate:\n- Review violation reports\n- Adjust policy to allow legitimate sources\n- Block malicious or unnecessary\nstep_4_enforce:\nheader: \"Content-Security-Policy\"\npurpose: \"Switch from report-only to enforcement\"\nstep_5_monitor:\n- Continued violation reporting via report-uri or report-to\n- Adjust as legitimate needs change\nFor Joseph's significant WordPress portfolio:\nwordpress_hardening:\ncore_updates:\nrequirement: \"WordPress core auto-updates enabled\"\ncadence: \"Weekly verification across portfolio\"\nautomation: \"Automatic for security patches; managed for major versions\"\nplugins:\nrule: \"Only install reputable plugins from wordpress.org or trusted sources\"\naudit: \"Quarterly plugin audit per site\"\nremove: \"Inactive plugins (security surface)\"\nupdate: \"Auto-updates enabled for security; managed for major versions\"\nthemes:\nrule: \"Reputable themes only\"\naudit: \"Inactive themes removed\"\ncustom_themes: \"Maintained and updated\"\nuser_accounts:\nadmin_username: \"Never 'admin' (default)\"\ndefault_role: \"Subscriber for new users\"\nrole_review: \"Quarterly review of administrators\"\ninactive_users: \"Removed periodically\"\npassword_security:\nrequirement: \"Strong passwords enforced\"\nplugin: \"Force Strong Passwords or similar\"\ntwo_factor: \"Enabled for administrators\"\nfile_permissions:\nfiles: \"644\"\ndirectories: \"755\"\nwp_config: \"600\"\nwp_config_hardening:\nkeys: \"Salts/keys regenerated; never default\"\ndb_prefix: \"Non-default ('wp_' is default; change to random)\"\ndebug: \"False in production\"\nxml_rpc:\ntypical: \"Disable unless required (mobile app, Jetpack)\"\nmethod: \".htaccess block or plugin\"\nrest_api:\npublic_endpoints: \"Restrict where appropriate\"\nplugin: \"Disable WP REST API or similar for restriction\"\nlogin_protection:\nrate_limiting: \"Limit Login Attempts plugin\"\ncaptcha: \"On login form\"\ntwo_factor: \"Wordfence 2FA, Google Authenticator, etc.\"\ncustom_login_url: \"WPS Hide Login or similar\"\nwordpress_security_plugin_options:\nwordfence:\ntype: \"Comprehensive security suite\"\nfeatures: [\"Firewall\", \"Malware scan\", \"Login protection\", \"2FA\"]\ncost: \"Free + Premium\"\nrecommended_for: \"Most WordPress sites\"\nsucuri:\ntype: \"Comprehensive security suite\"\nfeatures: [\"Firewall\", \"Malware scan\", \"Cleanup service\"]\ncost: \"Free plugin + paid services\"\nrecommended_for: \"Sites needing managed cleanup\"\npatchstack:\ntype: \"Vulnerability patching\"\nfeatures: [\"Virtual patching\", \"Vulnerability database\"]\ncost: \"Paid\"\nrecommended_for: \"Sites with many plugins\"\nithemes_security:\ntype: \"Comprehensive security suite\"\nfeatures: [\"Various hardening\", \"2FA\", \"Log monitoring\"]\ncost: \"Free + Pro\"\nrecommended_for: \"Alternative to Wordfence\"\nFor Joseph's hosting situation managing 130+ sites: standardizing on one security solution simplifies management. Wordfence Premium across portfolio with central monitoring is common pattern.\nwordpress_backup_strategy:\nfrequency:\nfiles: \"Daily for high-change sites; weekly for static-content\"\ndatabase: \"Daily minimum\"\nretention:\ndaily: \"Keep 7-30 days\"\nweekly: \"Keep 4-12 weeks\"\nmonthly: \"Keep 6-12 months\"\nstorage:\nrequirement: \"Off-server location\"\noptions:\n- S3 / Backblaze B2 / Wasabi\n- Other cloud storage\n- Different physical server\navoid: \"Backups only on same server (lost in compromise)\"\ntesting:\nquarterly: \"Test restore procedure\"\npurpose: \"Verify backups are actually viable\"\nplugins:\noptions:\n- UpdraftPlus (most popular)\n- BackWPup\n- Duplicator Pro\n- WP Time Capsule\nserver_side:\n- rsync to off-server location\n- Scripted backup to S3 or similar\nFor Joseph's self-managed Linux infrastructure:\n# /etc/ssh/sshd_config\n# Disable root login\nPermitRootLogin no\n# Use SSH keys only, no passwords\nPasswordAuthentication no\nChallengeResponseAuthentication no\n# Limit users who can SSH\nAllowUsers joseph backupuser\n# Change default port (security through obscurity, but reduces noise)\nPort 22042 # Or any non-standard port\n# Limit authentication attempts\nMaxAuthTries 3\nLoginGraceTime 30\n# Modern protocol only\nProtocol 2\n# Strong key exchange\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\n# Modern ciphers only\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nAfter changes:\nsshd -t # Test config\nsystemctl restart sshd\n# Basic UFW configuration\nufw default deny incoming\nufw default allow outgoing\n# Allow SSH (on custom port if changed)\nufw allow 22042/tcp\n# Allow web traffic\nufw allow 80/tcp\nufw allow 443/tcp\n# Enable firewall\nufw enable\n# Status\nufw status verbose\nPrevents brute force attacks:\napt install fail2ban\n# /etc/fail2ban/jail.local\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 3\n[sshd]\nenabled = true\nport = 22042\n[nginx-http-auth]\nenabled = true\n[wordpress]\nenabled = true\nfilter = wordpress\nlogpath = /var/log/nginx/access.log\n# Debian/Ubuntu unattended-upgrades\napt install unattended-upgrades\ndpkg-reconfigure -plow unattended-upgrades\n# Configure /etc/apt/apt.conf.d/50unattended-upgrades\n# Enable security updates\n# Configure email notifications\nlog_monitoring:\nwhat_to_m", "url": "https://wpnews.pro/news/web-security-headers-hsts-csp", "canonical_source": "https://dev.to/joseph_anady_214bacedf939/web-security-headers-hsts-csp-1bn5", "published_at": "2026-05-24 01:21:59+00:00", "updated_at": "2026-05-24 02:03:16.167943+00:00", "lang": "en", "topics": ["cybersecurity"], "entities": ["Google", "ThatDevPro", "WordPress", "Linux"], "alternates": {"html": "https://wpnews.pro/news/web-security-headers-hsts-csp", "markdown": "https://wpnews.pro/news/web-security-headers-hsts-csp.md", "text": "https://wpnews.pro/news/web-security-headers-hsts-csp.txt", "jsonld": "https://wpnews.pro/news/web-security-headers-hsts-csp.jsonld"}}