Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

curl Basic Auth: The Flag, the Header, and Keeping the Password Out of History

Sean

Platform Writer

Aug 17, 2026
8 min read

curl -u username:password https://example.com is the whole feature. curl base64-encodes the pair and sends it as an Authorization: Basic header. The interesting part is not the flag — it is that typing the password on the command line puts it in your shell history and in the process list, where other users on the machine can read it.

curl Basic Auth: The Flag, the Header, and Keeping the Password Out of History

Basic authentication is simple to the point of being crude: a username and password, base64-encoded, sent on every request. Base64 is encoding, not encryption, so the entire security of the scheme rests on TLS. Over HTTPS it is fine for machine-to-machine access. Over plain HTTP it is a credential broadcast.

Table of contents

The forms that work

# the standard flag
curl -u alice:secret https://api.example.com/v1/status
curl --user alice:secret https://api.example.com/v1/status

# omit the password and curl prompts, keeping it out of history
curl -u alice https://api.example.com/v1/status

# build the header yourself
curl -H "Authorization: Basic YWxpY2U6c2VjcmV0" https://api.example.com/v1/status

The last two are equivalent. -u does nothing more than base64-encode user:password and set that header, which is worth knowing when you are debugging against something that logs raw headers.

Generating the token by hand, if you need it for another tool:

printf 'alice:secret' | base64

# on Linux, avoid line wrapping on long credentials
printf 'alice:secret' | base64 -w 0

Use printf rather than echo. echo appends a newline by default, which gets encoded into the token and produces a credential the server rejects with no useful explanation. That trailing newline has cost a lot of people an afternoon.

The second form — -u alice with no colon — is the one to prefer interactively. curl prompts for the password, reads it without echoing, and it never touches your shell history.

Keeping the credential out of history and process lists

The command line is visible in two places you may not have considered: your shell’s history file, and the process table, where any user on the machine can see it with ps.

Options, in increasing order of safety:

# 1. prompt, as above
curl -u alice https://api.example.com

# 2. from an environment variable -- still visible in ps on some systems
curl -u "alice:$API_PASSWORD" https://api.example.com

# 3. from a netrc file -- nothing on the command line at all
curl -n https://api.example.com
curl --netrc-file ./secrets-netrc https://api.example.com

# 4. read the config from a file
curl --config ./curl-config https://api.example.com

The netrc route is the cleanest for repeated use. Create ~/.netrc with restrictive permissions:

machine api.example.com
  login alice
  password secret
chmod 600 ~/.netrc

curl reads it automatically with -n, matching on hostname. Nothing appears on the command line, so nothing lands in history or the process table. The 600 permission is not optional — many tools refuse to use a world-readable netrc, and the ones that do not should.

The --config file approach is the general version: put any curl options, including user = "alice:secret", in a file and reference it. Useful when you also want to pin headers and options for a particular API.

And prefix the command with a space if your shell is configured to ignore space-prefixed commands in history. It is a small habit that covers the one-off case.

The other authentication schemes

Basic is one of several curl supports, and the flag selects which.

curl --basic     -u alice:secret https://example.com
curl --digest    -u alice:secret https://example.com
curl --ntlm      -u alice:secret https://example.com
curl --negotiate -u alice:secret https://example.com
curl --anyauth   -u alice:secret https://example.com

# through a proxy
curl --proxy-user alice:secret --proxy http://proxy.example.com:8080 https://example.com

--anyauth makes curl probe the server and pick whichever method it advertises. Convenient, and it costs an extra round trip because curl sends an unauthenticated request first to read the challenge. Specify the method when you know it.

Digest was designed so the password never crosses the wire, which mattered before TLS was universal. Today it is largely legacy — over HTTPS, basic is simpler and no less secure in transit.

For modern APIs, bearer tokens are far more common than basic auth:

curl -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/me

# curl 7.61+ has a dedicated flag
curl --oauth2-bearer "$TOKEN" https://api.example.com/v1/me

A token can be scoped, expired, and revoked individually. A password cannot. Where an API offers both, use the token.

Debugging when it fails

# see the headers you sent and received
curl -v -u alice:secret https://api.example.com/v1/status

# response headers only
curl -I -u alice:secret https://api.example.com/v1/status

# include headers with the body
curl -i -u alice:secret https://api.example.com/v1/status

# full trace, including the exact bytes
curl --trace-ascii - -u alice:secret https://api.example.com

Reading the status code first narrows it immediately:

  • 401 Unauthorized — credentials missing, wrong, or the wrong scheme. Check the WWW-Authenticate response header, which names the scheme the server wants.
  • 403 Forbidden — authenticated successfully, but not permitted. A permissions problem, not a credentials one. These two get confused constantly and they point in different directions.
  • 404 on an authenticated endpoint — some APIs return 404 rather than 403 to avoid confirming a resource exists. Worth suspecting when the path is definitely correct.
  • Works with -u, fails with a hand-built header — nearly always a trailing newline from echo, or wrapped base64 output from a long credential.

One curl behaviour that surprises people: by default, credentials are not resent after a redirect to a different host, for good reason. --location-trusted overrides that and should be used only when you are certain about where the redirect leads.

Basic auth in a script

For anything automated, the priorities change from convenience to failing loudly.

#!/usr/bin/env bash
set -euo pipefail

: "${API_USER:?API_USER not set}"
: "${API_PASS:?API_PASS not set}"

response=$(curl --fail --silent --show-error \
  --netrc-file "$NETRC_FILE" \
  --max-time 30 \
  --retry 3 --retry-delay 2 \
  https://api.example.com/v1/status)

echo "$response" | jq -r '.state'
  • --fail returns a non-zero exit code on 4xx and 5xx instead of printing the error body and exiting zero. This is the flag that prevents a script from carrying on with an error page as its data.
  • --silent --show-error suppresses the progress meter but keeps real errors visible.
  • --max-time stops a hung request from stalling the script indefinitely.
  • --retry handles transient failures. Combine with --retry-connrefused when the target may still be starting.
  • The ${VAR:?message} form fails immediately with a clear message when a required variable is unset.

In CI, take credentials from the platform’s secret store rather than a file in the repository, and confirm the CI system masks them in logs. A curl -v in a build log with an Authorization header in it is a leaked credential, and build logs are frequently more widely readable than the repository.

When not to use basic auth

It has a narrow band where it is the right answer, and outside that band there are better options.

  • Never over plain HTTP. Base64 is trivially reversible; anyone on the path reads the password. If a service only offers HTTP, treat the credential as public.
  • Not for user-facing login. No logout, no session control, no expiry, credentials sent on every request. Use sessions or tokens.
  • Not where the credential cannot be rotated. Basic auth against a shared account with a password nobody can change is a liability that grows over time.
  • Not when the API offers tokens. Scoped, expiring, individually revocable credentials are strictly better.

Where it is genuinely fine: machine-to-machine over HTTPS against an internal service, a health check endpoint, a registry, or a quick manual test against an API that only supports it. Simple, universally supported, and adequate for exactly those cases.

How this fits the rest of the stack

The consistent thread here is that a credential’s biggest risk is not the protocol but where the value ends up sitting — shell history, a process list, an image layer, a build log. Environment variables supplied by the platform at runtime avoid most of that: on RunxBuild they are set per service in the dashboard, injected when the process starts, and changed without a rebuild, so rotating an API password is a restart rather than a new deploy. Services on RunxBuild covers environment variables alongside deploy logs and rollback. When you are sizing a service that talks to an authenticated API plus its own database, the RunxBuild hosting calculator shows each part as a separate line.

Useful related references:

FAQ

How do I send basic auth with curl?

Use curl -u username:password https://example.com. curl base64-encodes the pair and sends it as an Authorization: Basic header. Omit the password — curl -u username — and curl prompts for it instead, which keeps it out of your shell history.

How do I avoid putting my password in shell history?

Use -u username without the password so curl prompts, or put the credentials in ~/.netrc with 600 permissions and use curl -n. The netrc route puts nothing on the command line, so nothing appears in history or in the process table where other users can read it.

Why does my hand-built Authorization header fail when -u works?

Almost always a trailing newline. echo 'user:pass' | base64 encodes the newline that echo appends. Use printf 'user:pass' | base64 instead, and add -w 0 on Linux so long credentials are not wrapped across lines.

What is the difference between a 401 and a 403 from curl?

401 means the credentials are missing, wrong, or in the wrong scheme — check the WWW-Authenticate response header for what the server wants. 403 means you authenticated successfully but are not permitted to do that. They point at completely different fixes.

Is curl basic auth secure?

Only over HTTPS. Base64 is encoding, not encryption, so over plain HTTP the password is readable by anyone on the network path. Over TLS it is acceptable for machine-to-machine access, but a scoped bearer token is better wherever the API offers one, since it can expire and be revoked individually.

#curl basic auth#curl#http authentication#authorization header#api testing