curl HTTP Client

HTTP client operations and API testing.

HTTP Fundamentals

# HTTP methods
curl -X GET https://api.example.com/users            # Default, explicit GET
curl -X POST https://api.example.com/users           # Create
curl -X PUT https://api.example.com/users/1          # Update (full)
curl -X PATCH https://api.example.com/users/1        # Update (partial)
curl -X DELETE https://api.example.com/users/1       # Delete
curl -X HEAD https://api.example.com/users           # Headers only (like -I)
curl -X OPTIONS https://api.example.com/users        # CORS preflight check

# Response handling
curl -I https://example.com                          # Headers only
curl -i https://example.com                          # Headers + body
curl -s https://example.com                          # Silent (no progress)
curl -S https://example.com                          # Show errors with -s
curl -f https://example.com                          # Fail silently on HTTP errors
curl -L https://example.com                          # Follow redirects (302/301)
curl -L --max-redirs 5 https://example.com           # Limit redirect depth

# Output control
curl -o file.html https://example.com                # Save to specific file
curl -O https://example.com/file.tar.gz              # Save with remote filename
curl -o /dev/null -s https://example.com             # Discard body (timing tests)
curl -w '%{http_code}\n' -o /dev/null -s URL         # Just status code

# Connection tuning
curl --connect-timeout 5 https://example.com         # Connection timeout (seconds)
curl -m 30 https://example.com                       # Max total time
curl --retry 3 https://example.com                   # Retry on transient errors
curl --retry-delay 2 https://example.com             # Wait between retries

Headers and Content Types

# Setting headers
curl -H "Content-Type: application/json" URL
curl -H "Accept: application/json" URL
curl -H "X-Custom-Header: value" URL
curl -H "Host: api.example.com" http://10.0.0.1      # Override Host header

# Multiple headers
curl -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "X-Request-ID: $(uuidgen)" \
     URL

# Content negotiation
curl -H "Accept: application/xml" URL                 # Request XML
curl -H "Accept: text/plain" URL                      # Request plain text
curl -H "Accept-Language: es" URL                     # Spanish content
curl -H "Accept-Encoding: gzip" URL                   # Compressed response

# Common content types
# application/json          - JSON API
# application/xml           - XML/SOAP
# application/x-www-form-urlencoded - HTML form default
# multipart/form-data       - File uploads
# text/plain                - Raw text
# application/octet-stream  - Binary data

Authentication Patterns

# Basic auth (username:password base64 encoded)
curl -u user:pass https://api.example.com
curl -u admin https://api.example.com                # Prompt for password
curl -H "Authorization: Basic $(echo -n 'user:pass' | base64)" URL

# Bearer token (OAuth2, JWT)
curl -H "Authorization: Bearer $TOKEN" URL
curl -H "Authorization: Bearer $(cat /tmp/token)" URL

# API key patterns
curl -H "X-API-Key: $API_KEY" URL                    # Header-based
curl "https://api.example.com?api_key=$API_KEY"      # Query param (less secure)

# Digest auth
curl --digest -u user:pass https://api.example.com

# Client certificates (mTLS)
curl --cert /path/to/cert.pem --key /path/to/key.pem URL
curl --cert /path/to/cert.pem:password URL           # If key has passphrase
curl --cacert /path/to/ca.pem URL                    # Custom CA trust

# Negotiate (Kerberos/SPNEGO)
curl --negotiate -u : URL                            # Use Kerberos ticket

# AWS Signature v4
curl --aws-sigv4 "aws:amz:us-east-1:s3" \
     -u "$AWS_ACCESS_KEY:$AWS_SECRET_KEY" URL

POST Data and Forms

# URL-encoded form data (default Content-Type)
curl -d "username=admin&password=secret" URL
curl -d "key1=value1" -d "key2=value2" URL           # Multiple fields
curl --data-urlencode "query=hello world" URL        # URL-encode value

# JSON POST
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"secret"}' \
     URL

# JSON from file
curl -X POST \
     -H "Content-Type: application/json" \
     -d @/tmp/payload.json \
     URL

# JSON from stdin (heredoc)
curl -X POST \
     -H "Content-Type: application/json" \
     -d @- URL <<'EOF'
{
    "name": "test",
    "enabled": true,
    "tags": ["production", "critical"]
}
EOF

# File upload (multipart/form-data)
curl -F "file=@/path/to/file.pdf" URL
curl -F "file=@report.pdf;filename=monthly.pdf" URL  # Rename on upload
curl -F "file=@doc.pdf;type=application/pdf" URL     # Explicit MIME type
curl -F "file=@image.png" -F "description=logo" URL  # File + field

# Binary data
curl -X POST --data-binary @/path/to/binary.bin URL
curl -X POST --data-binary "@-" URL < /tmp/data      # From stdin

HashiCorp Vault API

# Environment setup
export VAULT_ADDR="https://vault-01.inside.domusdigitalis.dev:8200"
export VAULT_TOKEN="hvs.CAESIP..."  # Or from file: $(cat ~/.vault-token)

# Check Vault health (no auth needed)
curl -sk "$VAULT_ADDR/v1/sys/health" | jq
curl -sk "$VAULT_ADDR/v1/sys/seal-status" | jq '.sealed'

# KV v2 secrets (note /data/ in path)
curl -sk -H "X-Vault-Token: $VAULT_TOKEN" \
     "$VAULT_ADDR/v1/kv/data/myapp/config" | jq '.data.data'

# Write KV secret
curl -sk -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"data":{"username":"admin","password":"secret"}}' \
     "$VAULT_ADDR/v1/kv/data/myapp/config"

# PKI - Issue certificate
curl -sk -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"common_name":"myhost.inside.domusdigitalis.dev","ttl":"8760h"}' \
     "$VAULT_ADDR/v1/pki_int/issue/domus-client" | jq

# SSH CA - Sign public key
curl -sk -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
     -H "Content-Type: application/json" \
     -d "{\"public_key\":\"$(cat ~/.ssh/id_ed25519.pub)\",\"valid_principals\":\"ansible,evanusmodestus,root\"}" \
     "$VAULT_ADDR/v1/ssh/sign/domus-client" | jq -r '.data.signed_key' > ~/.ssh/id_ed25519-cert.pub

# List secrets (careful: LIST uses different path pattern)
curl -sk -X LIST -H "X-Vault-Token: $VAULT_TOKEN" \
     "$VAULT_ADDR/v1/kv/metadata/" | jq '.data.keys'

# AppRole authentication
curl -sk -X POST \
     -d '{"role_id":"'"$ROLE_ID"'","secret_id":"'"$SECRET_ID"'"}' \
     "$VAULT_ADDR/v1/auth/approle/login" | jq -r '.auth.client_token'

Cisco ISE ERS API

# Environment setup (use dsource to load these)
# dsource d000 dev/network
# Required: ISE_HOST, ISE_USER, ISE_PASS

ISE_BASE="https://$ISE_HOST:9060/ers/config"
ISE_AUTH="$ISE_USER:$ISE_PASS"

# NOTE: ISE requires both Accept AND Content-Type headers

# List endpoints
curl -sk -u "$ISE_AUTH" \
     -H "Accept: application/json" \
     "$ISE_BASE/endpoint" | jq '.SearchResult.resources[:5]'

# Get specific endpoint by ID
curl -sk -u "$ISE_AUTH" \
     -H "Accept: application/json" \
     "$ISE_BASE/endpoint/$ENDPOINT_ID" | jq '.Endpoint'

# Create endpoint
curl -sk -X POST -u "$ISE_AUTH" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
       "Endpoint": {
         "name": "Test Endpoint",
         "mac": "AA:BB:CC:DD:EE:FF",
         "staticGroupAssignment": true,
         "groupId": "'"$GROUP_ID"'"
       }
     }' \
     "$ISE_BASE/endpoint"

# Update endpoint
curl -sk -X PUT -u "$ISE_AUTH" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
       "Endpoint": {
         "id": "'"$ENDPOINT_ID"'",
         "name": "Updated Name",
         "mac": "AA:BB:CC:DD:EE:FF"
       }
     }' \
     "$ISE_BASE/endpoint/$ENDPOINT_ID"

# Delete endpoint
curl -sk -X DELETE -u "$ISE_AUTH" \
     -H "Accept: application/json" \
     "$ISE_BASE/endpoint/$ENDPOINT_ID"

# Search with filters
curl -sk -u "$ISE_AUTH" \
     -H "Accept: application/json" \
     "$ISE_BASE/endpoint?filter=mac.EQ.AA:BB:CC:DD:EE:FF" | jq

# List downloadable ACLs
curl -sk -u "$ISE_AUTH" \
     -H "Accept: application/json" \
     "$ISE_BASE/downloadableacl" | jq '.SearchResult.resources[].name'

Kubernetes API

# Get API server URL and token
K8S_API=$(kubectl config view -o jsonpath='{.clusters[0].cluster.server}')
K8S_TOKEN=$(kubectl get secret -n default \
    $(kubectl get sa default -o jsonpath='{.secrets[0].name}') \
    -o jsonpath='{.data.token}' | base64 -d)

# Or use service account token (inside pod)
K8S_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
K8S_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

# List pods
curl -sk -H "Authorization: Bearer $K8S_TOKEN" \
     "$K8S_API/api/v1/namespaces/default/pods" | jq '.items[].metadata.name'

# Get specific pod
curl -sk -H "Authorization: Bearer $K8S_TOKEN" \
     "$K8S_API/api/v1/namespaces/default/pods/mypod" | jq

# Create pod (from JSON)
curl -sk -X POST -H "Authorization: Bearer $K8S_TOKEN" \
     -H "Content-Type: application/json" \
     -d @pod.json \
     "$K8S_API/api/v1/namespaces/default/pods"

# Delete pod
curl -sk -X DELETE -H "Authorization: Bearer $K8S_TOKEN" \
     "$K8S_API/api/v1/namespaces/default/pods/mypod"

# Watch pods (Server-Sent Events)
curl -sk -H "Authorization: Bearer $K8S_TOKEN" \
     "$K8S_API/api/v1/namespaces/default/pods?watch=true"

# Get cluster info
curl -sk -H "Authorization: Bearer $K8S_TOKEN" \
     "$K8S_API/api/v1" | jq '.resources[].name'

# NOTE: For production, use kubectl or client libraries
# curl is useful for debugging API access issues

Wazuh API

# Environment setup (from dsource)
# dsource d000 dev/observability
# Required: WAZUH_API_URL, WAZUH_API_USER, WAZUH_API_PASSWORD

# Get JWT token (required for all API calls)
TOKEN=$(curl -sk -X POST \
    -H "Content-Type: application/json" \
    -d '{"username":"'"$WAZUH_API_USER"'","password":"'"$WAZUH_API_PASSWORD"'"}' \
    "$WAZUH_API_URL/security/user/authenticate" | jq -r '.data.token')

# Check manager status
curl -sk -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/manager/status" | jq '.data.affected_items'

# List agents
curl -sk -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/agents" | jq '.data.affected_items[] | {id, name, status}'

# Get agent details
curl -sk -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/agents/001" | jq '.data.affected_items[0]'

# Restart agent
curl -sk -X PUT -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/agents/001/restart"

# List rules
curl -sk -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/rules?limit=10" | jq '.data.affected_items[] | {id, description, level}'

# Get active alerts (last 24h)
curl -sk -H "Authorization: Bearer $TOKEN" \
     "$WAZUH_API_URL/alerts?limit=100&sort=-timestamp" | jq

Debugging and Timing

# Verbose output (-v = --verbose)
curl -v https://example.com 2>&1 | grep -E '^[<>*]'

# Even more verbose (TLS handshake details)
curl -vvv https://example.com

# Trace to file
curl --trace /tmp/curl-trace.log https://example.com
curl --trace-ascii /tmp/curl-trace.txt https://example.com  # Readable

# Timing information
curl -w "@-" -o /dev/null -s https://example.com <<'EOF'
    DNS Lookup:  %{time_namelookup}s
    TCP Connect: %{time_connect}s
    TLS Setup:   %{time_appconnect}s
    First Byte:  %{time_starttransfer}s
    Total Time:  %{time_total}s
    HTTP Code:   %{http_code}
    Size:        %{size_download} bytes
EOF

# One-liner timing
curl -w "Time: %{time_total}s | Code: %{http_code}\n" \
     -o /dev/null -s https://example.com

# Check HTTP response code only
http_code=$(curl -o /dev/null -s -w '%{http_code}' URL)
[[ "$http_code" == "200" ]] && echo "OK" || echo "FAIL: $http_code"

# Get effective URL (after redirects)
curl -Ls -o /dev/null -w '%{url_effective}' https://bit.ly/shorturl

# Show redirect chain
curl -sIL https://bit.ly/shorturl 2>&1 | grep -i 'location:'

TLS/SSL Operations

# Skip certificate verification (development only!)
curl -k https://self-signed.example.com
curl --insecure https://self-signed.example.com

# Use specific CA bundle
curl --cacert /path/to/ca-bundle.crt https://internal.example.com

# Client certificate authentication (mTLS)
curl --cert /etc/ssl/certs/client.pem \
     --key /etc/ssl/private/client.key \
     https://mtls.example.com

# With passphrase-protected key
curl --cert /path/to/cert.pem:passphrase \
     --key /path/to/key.pem \
     https://mtls.example.com

# Show certificate chain
curl -vvI https://example.com 2>&1 | grep -A2 'Server certificate'

# Force TLS version
curl --tlsv1.2 https://example.com                   # Minimum TLS 1.2
curl --tls-max 1.2 https://example.com               # Maximum TLS 1.2
curl --tlsv1.3 https://example.com                   # Require TLS 1.3

# Check certificate expiry
curl -svI https://example.com 2>&1 | grep 'expire date'

# Alternative: openssl for detailed cert inspection
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
    openssl x509 -noout -dates

Scripting Patterns

# Health check with retry
check_health() {
    local url=$1 retries=${2:-3} delay=${3:-5}
    for ((i=1; i<=retries; i++)); do
        if curl -sf -o /dev/null "$url"; then
            echo "OK: $url"
            return 0
        fi
        echo "Attempt $i/$retries failed, retrying in ${delay}s..."
        sleep "$delay"
    done
    echo "FAIL: $url unreachable after $retries attempts"
    return 1
}

# Usage
check_health "https://vault-01.inside.domusdigitalis.dev:8200/v1/sys/health"

# Parallel API calls (background jobs)
for endpoint in users groups policies; do
    curl -s "https://api.example.com/$endpoint" > "/tmp/$endpoint.json" &
done
wait  # Wait for all background jobs

# Rate-limited requests
for id in {1..100}; do
    curl -s "https://api.example.com/users/$id" >> /tmp/users.jsonl
    sleep 0.1  # 10 requests per second max
done

# API pagination
page=1
while true; do
    response=$(curl -s "https://api.example.com/users?page=$page&per_page=100")
    count=$(echo "$response" | jq '.data | length')
    [[ "$count" -eq 0 ]] && break
    echo "$response" | jq -c '.data[]' >> /tmp/all-users.jsonl
    ((page++))
done

# Error handling
response=$(curl -sf -w '\n%{http_code}' "https://api.example.com/data")
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -n -1)

case "$http_code" in
    200) echo "Success: $body" ;;
    401) echo "Auth failed" >&2; exit 1 ;;
    404) echo "Not found" >&2; exit 1 ;;
    5*)  echo "Server error: $http_code" >&2; exit 1 ;;
    *)   echo "Unexpected: $http_code" >&2; exit 1 ;;
esac

Infrastructure Patterns

# Multi-host health check
HOSTS=(
    "https://vault-01.inside.domusdigitalis.dev:8200/v1/sys/health"
    "https://ise-01.inside.domusdigitalis.dev:9060/ers/sdk"
    "https://wazuh.inside.domusdigitalis.dev:443"
    "https://grafana.inside.domusdigitalis.dev:443/api/health"
)

printf "%-50s %s\n" "ENDPOINT" "STATUS"
for url in "${HOSTS[@]}"; do
    code=$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "$url")
    status=$([[ "$code" =~ ^2 ]] && echo "✓ OK" || echo "✗ FAIL ($code)")
    printf "%-50s %s\n" "$url" "$status"
done

# Certificate expiry check across hosts
for host in vault-01 ise-01 wazuh; do
    echo "=== $host ==="
    echo | timeout 5 openssl s_client -connect "$host.inside.domusdigitalis.dev:443" \
        -servername "$host.inside.domusdigitalis.dev" 2>/dev/null | \
        openssl x509 -noout -dates | awk -F= '/notAfter/{print $2}'
done

# Download with resume support (large files)
curl -C - -O https://example.com/large-file.iso

# Mirror website (basic)
curl -O -L --remote-name-all https://example.com/{page1,page2,page3}.html

# Conditional download (only if modified)
curl -z /tmp/cached.json -o /tmp/cached.json https://api.example.com/data.json

# Webhook notification
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"text":"Deployment complete on '"$(hostname)"'"}' \
     "https://hooks.slack.com/services/XXX/YYY/ZZZ"

Common Gotchas

# WRONG: Variable in single quotes (not expanded)
curl -d '{"name":"$USER"}' URL

# CORRECT: Use double quotes for variables
curl -d '{"name":"'"$USER"'"}' URL
# Or use jq for safe JSON construction
curl -d "$(jq -n --arg name "$USER" '{name: $name}')" URL

# WRONG: Missing Content-Type for JSON
curl -X POST -d '{"key":"value"}' URL

# CORRECT: Always specify Content-Type
curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' URL

# WRONG: Assuming HTTP 200 means success
curl https://api.example.com/delete/123

# CORRECT: Check exit code AND HTTP status
if curl -sf -o /dev/null https://api.example.com/delete/123; then
    echo "Success"
else
    echo "Failed"
fi

# WRONG: Passwords in command line (visible in ps)
curl -u admin:secret URL

# CORRECT: Use netrc file or prompt
curl -n URL                                          # Use ~/.netrc
curl -u admin URL                                    # Prompt for password

# WRONG: Ignoring redirects
curl https://example.com                             # May get 301/302

# CORRECT: Follow redirects
curl -L https://example.com

# WRONG: Not URL-encoding special characters
curl "https://api.example.com/search?q=hello world"

# CORRECT: URL encode
curl --data-urlencode "q=hello world" https://api.example.com/search
curl "https://api.example.com/search?q=$(printf '%s' 'hello world' | jq -sRr @uri)"

Quick Reference

# Essential flags
-s              # Silent (no progress)
-S              # Show errors (with -s)
-f              # Fail silently on HTTP errors (non-zero exit)
-L              # Follow redirects
-k              # Skip TLS verification
-v              # Verbose output
-I              # HEAD request (headers only)
-i              # Include headers in output
-o FILE         # Output to file
-O              # Save with remote filename
-d DATA         # POST data
-H HEADER       # Add header
-u USER:PASS    # Basic auth
-X METHOD       # HTTP method
-m SECONDS      # Max time
--connect-timeout SECONDS
-w FORMAT       # Write-out format

# Common status codes
# 200 OK
# 201 Created
# 204 No Content
# 301 Moved Permanently
# 302 Found (temporary redirect)
# 400 Bad Request
# 401 Unauthorized
# 403 Forbidden
# 404 Not Found
# 429 Too Many Requests
# 500 Internal Server Error
# 502 Bad Gateway
# 503 Service Unavailable

# Write-out variables
# %{http_code}        - HTTP status code
# %{time_total}       - Total time in seconds
# %{time_connect}     - Time to connect
# %{time_namelookup}  - DNS lookup time
# %{size_download}    - Downloaded bytes
# %{url_effective}    - Final URL after redirects