Secure a public API

3 min read07 November 2026

An API reachable from the internet receives automated probes within minutes. Most fail, but they look for exactly what rushed development leaves open: a route without a token, a parameter concatenated into SQL, an overly broad CORS policy. API security is built in layers, each testable on its own.

Terminate TLS before the application

The application listens on 127.0.0.1, Nginx exposes port 443 and forwards the request. The application never sees plaintext public traffic and does not handle certificates:

Nginx
server {
    listen 443 ssl;
    server_name votre-domaine.fr;
    client_max_body_size 1m;

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Install the certificate with sudo certbot --nginx -d votre-domaine.fr, then check renewal with sudo certbot renew --dry-run. Enable HSTS only once you are sure HTTP is no longer served. On app hosting you do not manage Nginx: apply the same principles in code and inspect the headers on the allocation you receive.

Authenticate and authorize

An API key in the URL ends up in logs, browser history and proxies. Put it in the Authorization header, over HTTPS only. If you store keys, store a hash (Argon2id or bcrypt), not the plaintext value, and show the key once at creation.

Separate authentication from authorization: a valid token should not automatically allow everything. Use scopes (read, write, admin) and check them on every route. Prefer short-lived tokens with renewal, and plan immediate revocation for a compromised key.

Rate-limit and validate

A quota per address and per key protects against scanning and abuse:

Nginx
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {
    limit_req zone=api burst=20 nodelay;
    limit_req_status 429;
}

Validate every input server-side: type, length, format, allowed fields. Reject unknown fields instead of silently ignoring them. Use parameterized queries, never SQL concatenation. Cap the request body with client_max_body_size and set a read timeout so connections do not stay open.

CORS and logs

CORS is a browser rule, not authentication. Allow known origins explicitly; never use * together with credentials. Answer OPTIONS requests correctly and cache the preflight response.

Log the method, route, status, latency, a request identifier and the source address, but never the token, password or full body. A log without personal data stays usable and shareable. Alert on spikes of 401, 403 and 429 rather than on every error.

Verify

Bash
curl -sI https://votre-domaine.fr/api/health
curl -i https://votre-domaine.fr/api/health
curl -i -H "Authorization: Bearer jeton-invalide" https://votre-domaine.fr/api/health

Expected: valid HTTPS, 401 without a token, 429 after a burst. Make sure errors expose neither a full stack trace nor a framework version. Read the logs after a load test: if you cannot explain a request, the log is incomplete.

Advertisement

Was this article helpful?

Read next