Why we care: we are Geonode and we sell proxies, so we see a great many commands with credentials in them — often both a proxy password and a target password on the same line. The practical warning worth leading with is that credentials in a curl command end up in shell history, in process listings, and in whatever you paste into a support ticket. We have received screenshots containing live passwords more than once. The .netrc approach below fixes it in about a minute and costs nothing, and it applies equally to proxy credentials, which is covered near the end.
The Basic Syntax
curl -u username:password https://api.example.com/private
The curl manual documents -u, --user <user:password>: "Specify the username and password to use for server authentication."
Basic is the default scheme, so --basic is usually redundant. The manual says as much: "Use HTTP Basic authentication with the remote host. This method is the default and this option is usually pointless, unless you use it to override a previously set option that sets a different authentication method."
What actually goes on the wire is a header:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
That is username:password base64-encoded — encoded, not encrypted. Anyone who can see the request can decode it in one step. This is why basic authentication over plain HTTP is equivalent to sending your password in clear text, and why it should only ever be used over HTTPS.
One syntax constraint from the manual: "The username and passwords are split up on the first colon, which makes it impossible to use a colon in the username with this option. The password can, still." So a colon in a password is fine; a colon in a username is not.
Why the Command Line Is the Wrong Place
The manual does not hedge on this:
On systems where it works, curl hides the given option argument from process listings. This is not enough to protect credentials from possibly getting seen by other users on the same system as they still are visible for a moment before being cleared. Such sensitive data should be retrieved from a file instead or similar and never used in clear text in a command line.
Four separate exposures, all real:
Shell history. ~/.bash_history or the zsh equivalent, in plain text, indefinitely.
Process listings. Visible to other users on the machine during the brief window before curl clears it.
Logs. Anything that records the commands a script ran.
Pasted output. Bug reports, issue trackers, chat messages, screenshots.
The last one is the most common in practice and the least considered.
Three Safer Ways
1. Let curl prompt. Give only the username and curl asks for the password interactively, reading it without echoing:
curl -u username https://api.example.com/private
Nothing is stored, nothing is logged. This is the right approach for anything you type by hand.
2. Use a .netrc file. The manual describes -n, --netrc: "Make curl scan the .netrc file in the user's home directory for login name and password... If used with HTTP, curl enables user authentication."
Create ~/.netrc:
machine api.example.com
login myusername
password mypassword
Then restrict it, because curl will not do it for you — the manual notes that "curl does not complain if that file does not have the right permissions (it should be neither world- nor group-readable)":
chmod 600 ~/.netrc
curl -n https://api.example.com/private
Three useful details from the manual. "The netrc file provides credentials for a hostname independent of which protocol and port number that are used", so one entry covers a host. --netrc-file "overrides all other ways to figure out the file", which is handy for per-project credential files. And since curl 8.16.0, a NETRC environment variable can name the file. On Windows, both .netrc and _netrc are checked in the home directory, with the former preferred.
--netrc-optional is the variant that uses the file if present and does not fail if it is absent — better for scripts that may run in either state.
3. Read from an environment variable. When a file is impractical, at least keep it out of history:
read -rs API_PASS
curl -u "myuser:${API_PASS}" https://api.example.com/private
Note the -s on read so the password is not echoed. This is still visible in the environment of the process, so it is a middle option rather than a good one.
For scripts, .netrc with chmod 600 is the answer. It is the option the manual points you towards and the one that removes every exposure listed above.
Basic Versus the Other Schemes
curl supports several, and knowing which is which prevents a class of confusion.
| Option | Scheme | Password on the wire |
|---|---|---|
--basic | HTTP Basic (default) | Base64-encoded, effectively clear |
--digest | HTTP Digest | Hashed challenge-response |
--ntlm | NTLM | Windows environments |
--negotiate | SPNEGO / Kerberos | Ticket-based |
--anyauth | Automatic | Depends what is chosen |
--oauth2-bearer | Bearer token | The token itself |
Digest is documented as: "Enable HTTP Digest authentication. This authentication scheme avoids sending the password over the wire in clear text. Use this in combination with the normal --user option to set username and password."
--anyauth is the convenience option with a cost the manual states plainly: "Figure out authentication method automatically, and use the most secure one the remote site claims to support. This is done by first doing a request and checking the response-headers, thus possibly inducing an extra network round-trip."
An extra round trip per request is not free at volume. And there is a specific failure the manual warns about: "Using --anyauth is not recommended if you do uploads from stdin, since it may require data to be sent twice and then the client must be able to rewind. If the need should arise when uploading from stdin, the upload operation fails."
So: use --anyauth when you genuinely do not know what the server wants, and specify the scheme once you do.
Bearer tokens are what most modern APIs actually use, and they are not basic auth at all:
curl --oauth2-bearer "mF_9.B5f-4.1JqM" https://api.example.com/me
Equivalent to setting Authorization: Bearer ... by hand. Note that a bearer token is the credential — anyone holding it can use it — so it deserves the same handling as a password.
Reading the Response
A short diagnostic path when authentication does not work.
401 Unauthorized means the server wants credentials, or rejected the ones you sent. The response carries a WWW-Authenticate header naming the scheme it expects, and reading it saves guessing:
curl -sS -o /dev/null -D - https://api.example.com/private | grep -i www-authenticate
If it says Digest and you sent Basic, that is your answer.
403 Forbidden is different and often misread. You authenticated successfully and are not permitted to do this. Changing your password will not help; changing your permissions might.
407 Proxy Authentication Required means the proxy wants credentials, not the target. Different header, different option, covered next.
A 200 with a login page means the endpoint does not use HTTP authentication at all — it uses a form and a session cookie, and -u does nothing for it. Check the Content-Type: if you expected JSON and got text/html, this is likely what happened.
To confirm what you actually sent:
curl -v -u user:pass https://api.example.com/private 2>&1 | grep -i '^> authorization'
Remember the manual's warning that verbose output "might contain sensitive data, including usernames, credentials or secret data content" — redact before sharing.
Building the Header Yourself
Sometimes -u is not what you want, and knowing what it actually produces lets you work around its limitations.
When the username contains a colon. -u splits on the first colon, so a username like service:reader is impossible to express. Construct the header directly:
CRED=$(printf '%s' 'service:reader:mypassword' | base64 -w0)
curl -H "Authorization: Basic ${CRED}" https://api.example.com/private
Note printf rather than echo, which appends a newline that ends up inside the encoded credential and produces a puzzling 401. And base64 -w0 to prevent line wrapping — GNU base64 wraps at 76 characters by default, and a header with an embedded newline is a malformed request. On macOS, plain base64 does not wrap and the flag is unnecessary.
When you need the credential from a secret manager. Most secret tooling outputs to stdout, and keeping the value in a variable rather than a file limits its lifetime:
TOKEN=$(vault kv get -field=token secret/api)
curl -H "Authorization: Bearer ${TOKEN}" https://api.example.com/me
When you want the header in a config file rather than the command. curl reads options from ~/.curlrc, or from a file named with -K:
# api-auth.conf
--user "myuser:mypassword"
--header "Accept: application/json"
curl -K api-auth.conf https://api.example.com/private
Restrict the file with chmod 600. This is a reasonable middle ground when .netrc does not fit — for instance when you need a token rather than a username and password.
A caution about ~/.curlrc specifically. It applies to every curl invocation by that user, including ones you did not write. Putting credentials there means sending them to whatever host any script happens to request. Use a named file with -K for anything sensitive, and keep ~/.curlrc for harmless defaults such as --show-error and --location.
Proxy Authentication Is Separate
The distinction that causes the most confusion in our support queue.
Two independent credential sets may be in play: one for the proxy, one for the target. They use different headers, different status codes and different curl options.
curl -x http://proxy.example.com:9000 \
--proxy-user proxyuser:proxypass \
-u apiuser:apipass \
https://api.example.com/private
--proxy-user authenticates to the proxy; -u authenticates to the target. Getting them crossed produces a 407 when you expected a 401, or vice versa.
The proxy scheme has parallel options — --proxy-basic, --proxy-digest, --proxy-anyauth, --proxy-negotiate — mirroring the target-side ones.
Two practical notes.
Credentials in the proxy URL have the same exposure problem, plus one. -x http://user:pass@proxy:9000 puts the password in the command line and in any environment variable holding the proxy URL, which is where http_proxy typically lives. Percent-encode any @, : or / in the password, or the URL parser will split in the wrong place.
Distinguish which hop refused you rather than guessing:
curl -sS -o /dev/null -x "$PROXY" \
-w 'connect=%{http_connect} status=%{response_code}\n' \
https://api.example.com/private
connect=407 means the proxy rejected you and never reached the target. connect=200 status=401 means the proxy worked and the target wants credentials. Two different fixes.
And a note specific to proxy authentication: many providers offer IP allowlisting as an alternative to username and password. If your source address is stable, that removes the credential from your commands entirely, which is the cleanest solution available — no file, no environment variable, nothing to leak.
When the Site Does Not Use HTTP Authentication At All
A large share of "curl basic auth is not working" reports are cases where the site never used HTTP authentication in the first place.
How to tell. Request the protected URL without credentials and look at the response:
curl -sS -o /dev/null -D - https://example.com/dashboard
A 401 with a WWW-Authenticate header means HTTP authentication, and -u is the right tool. A 200 returning a login page, or a 302 redirecting to /login, means the site uses a form and a session cookie — and no amount of -u will help, because nothing is reading that header.
The form-login pattern instead. Post the credentials to the login endpoint, keep the cookies, and reuse them:
curl -c jar.txt -d "username=ada&password=secret" \
https://example.com/login
curl -b jar.txt https://example.com/dashboard
-c writes a cookie jar and -b reads one. Use both on subsequent requests (-b jar.txt -c jar.txt) if the server rotates the session cookie, which many do.
The complication you will hit: CSRF tokens. Most login forms include a hidden token that must be submitted with the credentials, and it is generated per session. That means a two-step sequence — fetch the form, extract the token, submit it with the cookies from step one:
TOKEN=$(curl -sS -c jar.txt https://example.com/login \
| grep -o 'name="csrf_token" value="[^"]*"' \
| cut -d'"' -f4)
curl -b jar.txt -c jar.txt \
-d "csrf_token=${TOKEN}" -d "username=ada" -d "password=secret" \
https://example.com/login
Grep-and-cut on HTML is fragile, and it is fine for a one-off diagnostic. For anything ongoing, check first whether the service offers an API with token authentication — it almost always does, and it will be dramatically less work than maintaining a scraper of your own login form.
And if the login requires JavaScript, curl cannot do it at all. That is not a curl limitation to work around; it is a signal to look for the API endpoint the page itself is calling, which you can find in your browser's network tab and reproduce directly.
People Also Ask
How do I use basic authentication with curl?
curl -u username:password URL. Basic is curl's default scheme, so --basic is redundant unless you are overriding a previously set method. Always use HTTPS, since basic auth base64-encodes rather than encrypts the credentials.
How do I make curl prompt for a password?
Give only the username: curl -u username URL. curl asks for the password interactively and does not echo it, so nothing reaches your shell history or process listings. This is the right approach for anything typed by hand.
Is curl basic auth secure?
Only over HTTPS. The credentials are base64-encoded, which is trivially reversible, so over plain HTTP they are effectively in clear text. Over TLS the transport protects them, and the remaining risk is where you store them on your own machine.
How do I store curl credentials in a file?
Use ~/.netrc with machine, login and password lines, then chmod 600 it and call curl with -n. curl does not warn about wrong permissions, so setting them is on you. --netrc-file points at an alternative location, and --netrc-optional avoids failing when no file exists.
Why does curl return 401 when my password is correct?
Several possibilities: the server expects a different scheme — check the WWW-Authenticate header — or the endpoint uses form login with cookies rather than HTTP auth, or your username contains a colon, which -u cannot express because it splits on the first one.
What is the difference between 401 and 403?
401 means you are not authenticated: no credentials, or wrong ones. 403 means you authenticated successfully and are not permitted to do this. Retrying with different credentials helps the first and not the second.
How do I authenticate to a proxy with curl?
--proxy-user user:password, which is separate from -u for the target. A 407 status means the proxy wants credentials; a 401 means the target does. If your source address is stable, ask your provider about IP allowlisting instead — it removes the credential from your commands entirely.
What does --anyauth do?
It makes curl detect the server's preferred scheme by sending a request and reading the response headers, then authenticating with the most secure method offered. The cost is an extra round trip per request, and the manual warns it can fail when uploading from stdin because the data may need to be sent twice.
Wrapping Up
The syntax takes a moment: -u username:password and you are authenticated, with basic as curl's default scheme. The part worth getting right is where the password lives.
curl's own manual is direct about it — credentials "should be retrieved from a file instead or similar and never used in clear text in a command line" — and the exposures it warns about are all real. Shell history keeps them indefinitely, process listings expose them briefly to anyone on the machine, and pasted terminal output has an uncanny ability to reach places you did not intend.
Two habits fix it. For interactive use, give only the username and let curl prompt. For scripts, put credentials in ~/.netrc with chmod 600 and use -n. Neither takes longer than typing the password did.
And keep the two authentication layers straight. A 401 comes from the target and wants -u; a 407 comes from the proxy and wants --proxy-user. If your address is stable, an allowlist removes the second credential from your commands altogether — which is the only truly safe place for a secret to be.
