Security Analysis

Website Security Best Practices: The Complete Implementation Guide

Knowing what to check is only half the picture. This guide walks through how to actually implement each security layer, from TLS hardening and security headers to WAFs, DNS records, and ongoing monitoring. Every section includes configuration examples you can apply today.

SiteSecurityScore Team·16 min read·Updated Apr 8, 2026
Server room with blue lighting representing website security infrastructure

Why security needs layers#

No single security measure protects a website completely. HTTPS encrypts the connection, but it does not stop a malicious script that was injected through a compromised dependency. A Content Security Policy blocks unauthorized scripts, but it does nothing against a brute force login attempt. A WAF filters malicious HTTP requests, but it cannot prevent an attacker from spoofing your domain in a phishing email.

Each layer covers a different attack surface. When one layer fails or is misconfigured, the others still hold. This is the principle of defense in depth, and it is the reason security professionals never rely on a single control. The layers this guide covers are:

  • TLS configuration: encrypts the connection and authenticates your server
  • Security headers: instructs the browser how to handle your content safely
  • Cookie security: protects session data from theft and misuse
  • Web application firewall (WAF): filters malicious traffic before it reaches your application
  • DNS and email records: prevents domain spoofing and unauthorized certificate issuance
  • Monitoring: catches regressions and misconfigurations before they become incidents

If you have not yet verified which of these layers your site already has in place, start with the companion guide: How to Check Your Website Is Secure. It walks through how to audit each layer. This guide picks up where that one ends and shows you how to implement each one.

Harden your TLS configuration#

A TLS certificate is the entry point, but the server's TLS configuration determines how strong that encryption actually is. A default installation often supports legacy protocols and weak cipher suites for backward compatibility. Hardening means stripping those out so only modern, audited options remain.

Disable TLS 1.0 and 1.1. Both versions have known vulnerabilities and are no longer supported by modern browsers. Your server should only accept TLS 1.2 and TLS 1.3. TLS 1.3 is the preferred version because it removes vulnerable cipher suites entirely and completes the handshake in fewer round trips, making connections both faster and more secure.

Nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
Apache (.conf)
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on

Serve the full certificate chain. Your server must send its own certificate along with any intermediate certificates. If intermediates are missing, some browsers and mobile devices will fail to validate the connection even though the certificate itself is legitimate. Most certificate authorities provide a "full chain" or "bundle" file. Configure your server to use it.

Nginx
ssl_certificate /etc/ssl/certs/yourdomain-fullchain.pem;
ssl_certificate_key /etc/ssl/private/yourdomain.key;

Automate certificate renewal. Let's Encrypt certificates expire every 90 days. If renewal fails silently, your site goes down with a certificate error. Set up a cron job or systemd timer that runs certbot renew and verify it with a test run. If your hosting provider handles certificates, confirm that auto-renewal is enabled in their dashboard.

After making changes, verify your configuration with the TLS handshake checker to confirm the correct protocol versions and cipher suites are in use. For a deeper understanding of how browsers and servers negotiate the connection, see the TLS handshake guide.

Deploy security headers#

Security headers are HTTP response headers that instruct browsers how to handle your content. Adding them is a configuration change on your server or CDN. No application code changes are needed. The five headers below close the most common browser-side attack vectors.

Content-Security-Policy (CSP). This is the most powerful and most complex security header. It defines an allowlist of sources the browser is permitted to load scripts, styles, images, and other resources from. Everything not on the list is blocked. A well-configured CSP is the strongest defense against cross-site scripting (XSS).

Start in report-only mode so you can see what the policy would block without actually breaking anything. Once the reports are clean, switch to enforcement.

Nginx (report-only first)
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
Node.js / Express
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
  );
  next();
});

Once you are confident the policy works, remove -Report-Only from the header name to enforce it. If you use React, Next.js, or another framework that requires nonces or strict-dynamic, see the CSP for React and Next.js guide. To build a policy interactively, use the CSP generator.

Strict-Transport-Security (HSTS). Forces browsers to only connect over HTTPS. Once a browser sees this header, it will refuse HTTP connections to your domain for the duration of the max-age value, even if the user clicks an old HTTP link.

Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Apache (.htaccess)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

Start with a short max-age (like 300 seconds) while testing, then increase to one year (31536000) once confirmed. Add includeSubDomains only after verifying all subdomains support HTTPS. Use the HSTS generator to configure your header step by step. For a full explanation, see What is HSTS and Why It Matters.

X-Content-Type-Options. Prevents browsers from MIME sniffing, which is when the browser guesses a file's type by examining its contents instead of trusting the server's declared type. This one-line header stops uploaded files from being interpreted as executable scripts.

Nginx
add_header X-Content-Type-Options "nosniff" always;

X-Frame-Options. Prevents your site from being embedded in iframes on other domains. This blocks clickjacking, where attackers overlay invisible elements on top of your page to trick visitors into performing actions they did not intend.

Nginx
add_header X-Frame-Options "SAMEORIGIN" always;

Referrer-Policy. Controls how much URL information your site shares when visitors navigate to external links or when external resources load. The strict-origin-when-cross-origin value is a good default: it sends the origin (domain) to external sites but keeps the full URL for same-origin navigation.

Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

For a complete copy-paste reference with all server platforms, see the security headers cheat sheet. After deploying, run a free scan to confirm every header is present and correctly configured.

Lock down your cookies#

Cookies carry session identifiers, authentication tokens, and user preferences. If they are not locked down with the right attributes, they become one of the easiest targets for an attacker. Three attributes should be on every session cookie.

HttpOnly prevents JavaScript from reading the cookie. If an attacker injects a script through an XSS vulnerability, the script cannot access HttpOnly cookies. This is the single most important cookie attribute for session security.

Secure ensures the cookie is only transmitted over HTTPS connections. Without it, a session cookie could be sent in plain text if the browser makes an HTTP request through a redirect or a mixed content resource.

SameSite controls whether the cookie is sent with cross-site requests. Setting it to Lax prevents the cookie from being included in most cross-site POST requests, which is the foundation of CSRF protection. Strict is more restrictive but can break legitimate flows like returning from a third-party payment page.

Node.js / Express (express-session)
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));
Apache (mod_headers)
Header always edit Set-Cookie ^(.*)$ "$1; HttpOnly; Secure; SameSite=Lax"

Roll out cookie changes carefully

Adding the Secure flag will prevent the cookie from being sent over HTTP. If any part of your site still serves traffic over plain HTTP, users will lose their sessions on those pages. Confirm all traffic is on HTTPS before enabling this flag. Start with session cookies, verify they work, then extend to other cookies.

For implementation details across more frameworks, including Django, Laravel, and raw PHP, see the cookie security guide.

Deploy a web application firewall#

Security headers protect on the client side by instructing the browser. A web application firewall (WAF) protects on the server side by inspecting incoming HTTP requests and blocking those that match known attack patterns. The two work together: headers stop the browser from executing unauthorized content, and the WAF stops malicious requests from reaching your application in the first place.

What a WAF blocks. A well-configured WAF protects against the OWASP Top 10 attack categories, including SQL injection, cross-site scripting payloads in form fields, command injection, path traversal, and malformed HTTP requests. It also provides rate limiting to slow down brute force login attempts and bot traffic.

Cloud-based vs. self-hosted. Cloud-based WAFs like Cloudflare, AWS WAF, and Sucuri sit in front of your server as a reverse proxy. Traffic passes through the WAF before reaching your origin. They are the easiest to deploy because they require no changes to your application. Self-hosted options like ModSecurity (available for Apache and Nginx) run on your own server and give you more granular control over rules, but require more maintenance.

Start in monitoring mode. Every WAF has a mode where it logs requests that would be blocked without actually blocking them. Run in this mode for at least a week to identify false positives. Legitimate traffic that happens to match a rule pattern (like a blog post containing SQL syntax) will show up in the logs. Whitelist these before switching to active blocking.

Rate limiting. Even a basic WAF should enforce rate limits on login endpoints, API routes, and form submissions. A common starting configuration:

  • Login endpoints: 10 requests per minute per IP
  • API endpoints: 60 requests per minute per key or IP
  • Contact and registration forms: 5 submissions per minute per IP
  • General page requests: 200 requests per minute per IP

A WAF is not a substitute for secure code

WAF rules are pattern-based and can be bypassed with creative payloads. They catch the obvious attacks and raise the bar for attackers, but they do not replace input validation, parameterized queries, and output encoding in your application code. Treat the WAF as a safety net, not the primary defense.

DDoS protection. Most cloud-based WAFs include DDoS mitigation as part of their service. If you use a self-hosted WAF, consider adding a CDN or DDoS protection service in front of it. Volumetric attacks can overwhelm your server's capacity regardless of how well your application code is written.

Secure your DNS and email#

Your domain's DNS records are the foundation of trust on the internet. Without the right records, anyone can send email that appears to come from your domain, and any certificate authority in the world can issue a TLS certificate for it. These records are free to publish and take minutes to configure.

SPF (Sender Policy Framework). Publish a TXT record that lists the mail servers authorized to send email on behalf of your domain. When a receiving server gets an email claiming to be from your domain, it checks this record and can reject the message if the sender is not on the list.

DNS TXT Record
v=spf1 include:_spf.google.com include:sendgrid.net ~all

Replace the include: entries with your actual mail providers. The ~all (soft fail) is a safe starting point. Once you have confirmed your legitimate email is passing, switch to -all (hard fail) for stricter enforcement.

DKIM (DomainKeys Identified Mail). Adds a cryptographic signature to every outgoing email. Your mail provider generates a public/private key pair. The public key is published as a DNS TXT record, and the private key signs each message. Receiving servers verify the signature to confirm the email was not modified in transit.

DNS TXT Record (selector: google)
google._domainkey.yourdomain.com  TXT  "v=DKIM1; k=rsa; p=MIGfMA0GCS...your-public-key..."

Your email provider (Google Workspace, Microsoft 365, SendGrid, Postmark) will give you the exact record to publish. Follow their documentation to get the correct selector name and key value.

DMARC (Domain-based Message Authentication). Ties SPF and DKIM together with a policy. It tells receiving servers what to do when an email fails both SPF and DKIM checks, and it sends you reports showing who is sending email using your domain.

DNS TXT Record
_dmarc.yourdomain.com  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; pct=100"

Start with p=none to collect reports without affecting delivery. After reviewing reports and confirming all legitimate email passes authentication, move to p=quarantine and eventually p=reject.

CAA (Certificate Authority Authorization). Restricts which certificate authorities can issue certificates for your domain. Without this, any of the hundreds of public CAs can issue a valid certificate, increasing risk if one is compromised.

DNS CAA Record
yourdomain.com.  CAA  0 issue "letsencrypt.org"
yourdomain.com.  CAA  0 issuewild "letsencrypt.org"

Replace letsencrypt.org with the CA you actually use. The issuewild tag controls wildcard certificate issuance separately. For a complete walkthrough of all DNS security records, see the DNS security guide.

Monitor continuously#

Every layer described above can silently break. A server update resets your TLS configuration. A CDN migration drops your security headers. A developer adds a new cookie without the Secure flag. A certificate renewal fails and nobody notices until customers report errors. Security is not a one-time task. It requires ongoing verification.

Scan after every deployment. Build a scan into your CI/CD pipeline or make it part of your deployment checklist. A post-deployment scan catches misconfigurations while the change is still fresh and easy to revert. This does not need to be complex. A single HTTP request that checks for the expected headers and TLS version is enough to catch most regressions.

Automate daily scans. Manual checks rely on someone remembering to run them. Automated monitoring runs every day regardless and alerts you when something changes. Certificate expiry, missing headers, weakened TLS configuration, and DNS record changes can all be detected automatically.

Set up alerts. A scan is only useful if someone sees the results. Configure email alerts or integrate with your existing incident response workflow so that regressions are flagged within hours, not weeks. For a deeper look at what monitoring catches and how to set it up, see the guides on why continuous monitoring matters and how to monitor your website security automatically.

FAQ#

What is the most important website security measure?

There is no single most important measure. Website security works in layers. HTTPS encrypts the connection, security headers instruct the browser how to handle content, cookie flags protect sessions, a WAF filters malicious traffic, and DNS records prevent domain spoofing. Skipping any one layer leaves a gap that attackers can exploit.

Do I need a WAF if I already have security headers?

Yes. Security headers and WAFs protect against different things. Headers instruct the browser how to behave, which stops client-side attacks like XSS and clickjacking. A WAF inspects incoming requests on the server side, blocking SQL injection, credential stuffing, and other attacks that never reach the browser. They complement each other.

How do I secure my website without breaking it?

Roll changes out one layer at a time and test after each one. Start with TLS configuration, then add security headers individually. Use Content-Security-Policy-Report-Only to test CSP before enforcing it. Add cookie flags to session cookies first. Deploy a WAF in monitoring mode before switching to blocking. This incremental approach catches problems early without risking a site-wide outage.

How much does it cost to secure a website?

Most of the measures in this guide are free. TLS certificates from Let's Encrypt cost nothing. Security headers and cookie flags are server configuration changes. DNS records are free to publish. WAFs range from free tiers (Cloudflare) to paid enterprise solutions. The primary cost is the time to configure and test each layer.

References

Was this helpful?

See Where You Stand

Run a free scan to check your headers, TLS, cookies, and DNS in one go.