In this episode we'll prepare curl for production: TLS verification without shortcuts, secure secrets management, measured timeouts and retries, a disciplined update habit, and a security checklist for backends covering SSRF, redirects, audit logs, and transfer monitoring.

In episode 18 you learned to read failures, and in episode 20 you saw that curl and libcurl can live inside an application. Now comes the most important question: are your requests safe for production? On a laptop, a careless flag only makes your own request fail. On a production server — or inside a backend serving users — the same mistake can become a real security hole.
The key concept: every curl flag is a security decision. curl's defaults are already fairly safe, but many "just get it done" habits — disabling TLS verification, writing passwords on the command line, no timeouts — are exactly what create the gaps. Episode 21 is a hardening checklist: mandatory habits, correct techniques, and the traps you must avoid.
Certificate verification is the only thing ensuring you're talking to the right server, not a fraud in the middle. Since episode 12 you know curl does it by default — and in episode 18 we asserted to never disable it with -k.
# DON'T do this in production
curl -k https://api.example.com/secureInstead of disabling verification, fix what's wrong. Update the system CA bundle so it recognizes new CAs:
curl --cacert /etc/ssl/certs/ca-certificates.crt https://api.example.com/secureIf you're writing a client that must prove its own identity to the server (mutual TLS), use your certificate and key pair:
curl --cert client.pem --key client-key.pem https://api.example.com/secureOne more often-overlooked decision: bound the protocol version. --tlsv1.2 prevents the connection from falling back to fragile legacy TLS, and --tls-max 1.3 ensures you don't accept a version older than allowed. curl's defaults are sensible, but asserting the bounds removes ambiguity from the configuration.
Warning
-k / --insecure isn't just "less tidy" — it opens the door to man-in-the-middle attacks. Anyone on your network path (ISP, proxy, public wifi network, attacker) can slip in a fake certificate and read the entire contents of your request. If an endpoint forces you to use -k, be suspicious of that endpoint first — it's not the key that should be thrown away.
The most common real-world mistake is writing credentials directly on the command line. The problem isn't just shell history — a process's argument contents are visible to all users via ps while the process runs. In CI, the command line is even recorded in pipeline logs.
# Password appears in ps, shell history, and CI logs
curl -u arman:sandirahasia https://api.example.com/meThe correct solution is separating credentials from the command. The simplest option: environment variables, which never appear in ps:
curl -u "$API_USER:$API_PASS" https://api.example.com/meFor password-based authentication to many hosts, .netrc is a clean solution — curl reads it when invoked with --netrc, without credentials in any argument:
machine api.example.com
login arman
password sandirahasiacurl --netrc-file ~/.api-netrc https://api.example.com/meImportant
The .netrc file stores passwords in plain text, so its permissions must be 600 (chmod 600 ~/.api-netrc) so only its owner can read it. Never commit this file to a repository, and if needed, generate its contents from a secret manager during the deploy process — not stored as a static file.
A production request without a timeout is the culprit behind hanging scripts and exploding database connections. Every request must have a limit:
curl --silent --show-error --fail-with-body \
--connect-timeout 5 --max-time 30 \
--retry 3 --retry-delay 2 --retry-all-errors \
https://api.example.com/ordersLet's dissect the decision behind each flag:
--connect-timeout 5 — gives up after 5 seconds if a connection doesn't form. This prevents scripts from hanging when a host is unreachable.--max-time 30 — the total limit for the entire transfer. This protects against servers that answer slowly but never give up.--retry 3 --retry-delay 2 --retry-all-errors — retries failed transfers with pauses, while still having a bound so it doesn't become an endless loop.--silent --show-error — mutes the progress meter but keeps error messages visible.--fail-with-body — exits non-zero for 4xx/5xx statuses, so scripts can detect HTTP failures, not just transfer failures.One more layer for transfers that "run but stall": --speed-limit and --speed-time cut connections that slow below a certain threshold. A download silently stalled for hours is no longer a mystery — with this flag pair, curl considers it failed and retries (or exits) per your policy.
The monthly curl releases (covered in episode 19) have one unavoidable consequence: security fixes arrive fast, and you must pick them up. curl doesn't update itself — there's no auto-update. The version installed on your server is the version responsible for your transfer security.
Make version checking part of your routine:
curl --versionOn Debian/Ubuntu distros just apt update && apt upgrade curl; on RHEL/Fedora use dnf update curl. Though it looks trivial, this update habit is what closes CVE gaps on time. If you use curl/libcurl inside an application (episode 20), update it along with the other dependencies — stale bindings often cling to outdated libcurl versions. And on the flip side: don't blindly install the latest version on production servers without testing — but don't ignore security updates for months either. Stability and security are a balance, not an either/or choice.
When curl/libcurl lives inside a backend, the attack surface is different — and more dangerous. Here are four items that must be on the checklist.
Server-Side Request Forgery (SSRF) happens when an application uses user input to determine the URL the server requests. An attacker can point your server at http://169.254.169.254/ — the cloud metadata address — or at an internal service that shouldn't be reachable from outside. Before curl/libcurl touches a URL, do validation: allowlist permitted hosts, and block private, loopback, and link-local addresses.
HOST=$(printf '%s' "$1" | sed -E 's#^https?://##; s#/.*$##')
case "$HOST" in
127.*|10.*|192.168.*|169.254.*) echo "URL ditolak" >&2; exit 1 ;;
esac
curl --silent --fail "$1"The guard above is a simple example; in production, validation must happen inside the application, not just in the shell. The golden rule: the server must not be asked to fetch a URL controllable by users without strict filtering.
Unwatched redirects can carry your request anywhere — including to unexpected hosts. If your request follows redirects (-L), set a bound and check the final destination:
curl -L --max-redirs 3 https://api.example.com/loginBesides --max-redirs, consider --proto-redir to limit the protocols allowed when following redirects — for example rejecting a downgrade from https to http. If your application logic depends on the destination URL, revalidate after the redirect, not just at the initial URL.
Every production request must leave a trail. Record the destination host, status code, timing, and exit code — without recording tokens or sensitive bodies. With -w from episode 18, a complete log line can be produced without extra tools:
curl -sS -o /dev/null \
-w "host=%{url_effective} code=%{http_code} total=%{time_total}s size=%{size_download}\n" \
https://api.example.com/health >> /var/log/api-health.logThis log answers "what happened yesterday at 02:00?" in one second — far more valuable than guessing from memory. Don't forget to also log curl's exit code (non-zero) explicitly, because that's the signal that a transfer never completed.
Logs are only useful if someone reads them. Make transfer metrics part of monitoring: total time per request, errors per minute, and transfer size. A healthy pattern: a spike in time_total or repeated exit code 28 (timeout) is the first alarm before a real outage. Monitoring scripts using --fail-with-body will fail clearly when a service is troubled — and that's exactly what you want from an alarm.
Episode 21 closes the security side with five pillars: TLS verification without compromise, secrets that never touch command arguments, measured timeouts and retries, a disciplined update habit, and the backend checklist — SSRF, redirects, audit logs, and transfer monitoring.
The core takeaway: curl security isn't about features, but about habits. Its defaults are already safe; your job is to not break them for convenience, and to assert the bounds explicitly.
In episode 22 — the final episode — we'll close the entire journey: the ecosystem, best practices, and final reflections from 23 episodes of Learn Curl. See you!