Sitting behind a corporate egress proxy on Rocky Linux is one of those afternoons that teaches you why RHEL proxy settings live in four different files and none of them agree with the others. This guide walks through every layer that actually matters: shell env vars, /etc/profile.d, DNF, curl, wget, git, Podman, and systemd service drop-ins. Tested on a clean Rocky Linux 10.1 box, with the same steps applying unchanged to AlmaLinux 10, RHEL 10, and (with the DNF 5 config path) Fedora 42.
The focus is on system-wide behavior, not a one-off shell session. Every step you run will survive a reboot and apply to every user account on the box, including services started by systemd. If you need the same thing on Debian or Ubuntu, there is a companion guide: How To Set System Wide Proxy on Ubuntu / Debian Linux.
Tested April 2026 on Rocky Linux 10.1 (kernel 6.12.0) with DNF 4.20, curl 8.12, wget 1.24, git 2.47, Podman 5.6. Same steps apply to AlmaLinux 10 and RHEL 10. Fedora 42 uses DNF 5, noted inline where the config path changes.
Prerequisites
You need a Rocky Linux 10, AlmaLinux 10, RHEL 10, or Fedora 42 host with root or sudo access. Older releases (Rocky 9, RHEL 8, CentOS Stream 9) use the same file layout, so the commands below are portable back to those releases without modification.
A reachable HTTP or HTTPS proxy endpoint is required. It can be a corporate Squid, a self-hosted Squid on Rocky Linux, or a cloud-managed egress. If you are building from scratch, DigitalOcean droplets give you a cheap RHEL-family VM for testing, and a fresh Rocky 10 install takes less than ten minutes.
Step 1: Set reusable shell variables
Every command in this guide references the same proxy URL. Pulling it into a shell variable once keeps paste-and-go clean and prevents the “I edited 20 places and one still points to the old host” failure mode. Open a root shell on the target box and export the variables:
export PROXY_URL="http://proxy.example.internal:3128"
export NO_PROXY_LIST="localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,.example.com"
Swap proxy.example.internal:3128 for the real hostname and port of your proxy. If the proxy needs credentials, the format is http://user:pass@host:port, and anything with special characters must be URL-encoded (a @ in the password becomes %40, and so on). Confirm both variables are set before continuing:
echo "Proxy: ${PROXY_URL}"
echo "No proxy: ${NO_PROXY_LIST}"
These variables are session-scoped. Once you reconnect or drop into sudo -i, they disappear. Re-export them in every new shell where you follow along.
Step 2: Test the proxy in the current shell
Before touching any config file, prove the proxy variables actually change network behavior. With no proxy set, the environment is empty:
env | grep -i proxy
Now export the standard four variables. Both uppercase and lowercase forms are needed because different tools pick different conventions (curl reads lowercase, Java reads uppercase, Python reads either):
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
Verify the environment is populated:
env | grep -i proxy | sort
You should see the six lines echoed back with the real proxy URL. Now run a curl against an external host in verbose mode. The first few lines of output tell you whether curl found the proxy variable or not:
curl -v --max-time 5 https://example.com 2>&1 | head -10
On our Rocky 10 test box, with the dummy proxy above, curl prints the proxy it resolved from the environment:
* Uses proxy env variable HTTPS_PROXY == 'http://proxy.example.internal:3128'
* Trying ...
* Could not resolve proxy: proxy.example.internal
* shutting down connection #0
curl: (5) Could not resolve proxy: proxy.example.internal
The “Could not resolve proxy” error is expected for this guide because the proxy hostname is fictional. When pointed at a real proxy, the line changes to a successful CONNECT handshake. The important bit is that first line: Uses proxy env variable HTTPS_PROXY. That proves the system-wide env mechanism works. If you do not see that line when using a real proxy, your variables are not set in the shell that ran curl.
Step 3: Make the proxy permanent via /etc/profile.d
Environment variables set in a shell die with the shell. For a system-wide, boot-persistent proxy that every user inherits, drop a script into /etc/profile.d. Every login shell on Rocky, Alma, RHEL, and Fedora sources every *.sh file in that directory, so one file covers every user and every future SSH session.
sudo tee /etc/profile.d/proxy.sh > /dev/null <<'EOF'
# System-wide HTTP/HTTPS proxy settings
export HTTP_PROXY="http://proxy.example.internal:3128"
export HTTPS_PROXY="http://proxy.example.internal:3128"
export NO_PROXY="localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,.example.com"
# Lowercase duplicates for tools that expect them
export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export no_proxy="$NO_PROXY"
EOF
sudo chmod 644 /etc/profile.d/proxy.sh
Replace the literal proxy URL in the heredoc with your real one. A quick sed keeps the rest of the session clean:
sudo sed -i "s|http://proxy.example.internal:3128|${PROXY_URL}|g" /etc/profile.d/proxy.sh
sudo sed -i "s|localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,.example.com|${NO_PROXY_LIST}|" /etc/profile.d/proxy.sh
Source the file to load the values into the current shell without a logout cycle, then confirm:
source /etc/profile.d/proxy.sh
env | grep -i proxy | sort
Six lines are returned, matching what Step 2 produced manually. From this point every new SSH session, every new sudo shell, and every new tmux pane starts with the proxy already exported.
Step 4: Configure DNF proxy for package operations
DNF does not read environment variables for its HTTP traffic. It uses its own config file. Without this step, dnf install and dnf update will bypass the proxy entirely and fail on any network-restricted host. Edit /etc/dnf/dnf.conf and add a proxy= line under the [main] section:
echo "proxy=${PROXY_URL}" | sudo tee -a /etc/dnf/dnf.conf
If the proxy needs credentials, DNF also understands proxy_username and proxy_password as separate keys, which is cleaner than embedding the password in the URL:
proxy=http://proxy.example.internal:3128
proxy_username=dnfclient
proxy_password=change_me_now
Confirm the full /etc/dnf/dnf.conf looks correct:
cat /etc/dnf/dnf.conf
On a stock Rocky 10 install, the file ends with your new line appended:
[main]
gpgcheck=1
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
proxy=http://proxy.example.internal:3128
Test the new setting with a verbose metadata refresh. DNF logs the proxy on every HTTP call:
sudo dnf clean all
sudo dnf makecache --refresh 2>&1 | head -20
With the dummy proxy on our test box, DNF reports the failure by name, which is the signal you want: it was using the proxy, it just could not reach a real one:
Rocky Linux 10 - BaseOS 0.0 B/s | 0 B 00:00
Errors during downloading metadata for repository 'baseos':
- Curl error (5): Could not resolve proxy name for https://mirrors.rockylinux.org/...
Error: Failed to download metadata for repo 'baseos': Cannot prepare internal mirrorlist:
Curl error (5): Could not resolve proxy name for https://mirrors.rockylinux.org/...
Pointed at a real Squid the same command succeeds and caches metadata. Fedora 42 ships DNF 5, which reads the same proxy= key from /etc/dnf/dnf.conf, so the stanza is unchanged. The only Fedora-specific difference is the auto-upgrade tool dnf5-automatic, which inherits the proxy from the main config without extra work.
RHEL hosts also need the proxy for subscription-manager. The relevant file is /etc/rhsm/rhsm.conf; set proxy_hostname, proxy_port, and optionally proxy_user plus proxy_password under the [server] section. This is only required on RHEL, not on Rocky, Alma, or Fedora.
Step 5: Configure curl proxy via ~/.curlrc
Curl reads HTTPS_PROXY and HTTP_PROXY from the environment, so if you stopped at Step 3 curl already works. But a per-user ~/.curlrc file is useful for two cases: when you want curl to use a proxy in scripts that explicitly clear the environment (systemd ExecStart lines, cron jobs with a clean PATH), and when different users on the box need different proxy behavior. Create the file:
cat > ~/.curlrc <<EOF
proxy = ${PROXY_URL}
noproxy = ${NO_PROXY_LIST}
EOF
chmod 600 ~/.curlrc
Test without any env vars set so you know the config file alone carries the weight:
unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
curl -v --max-time 5 https://example.com 2>&1 | head -10
The verbose output no longer mentions an env var, but it still attempts to connect via the proxy. When pointing at a real Squid the CONNECT succeeds; with our placeholder host the DNS lookup fails:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
* Could not resolve proxy: proxy.example.internal
* shutting down connection #0
curl: (5) Could not resolve proxy: proxy.example.internal
One footgun: the keys in ~/.curlrc are proxy and noproxy, not http_proxy. Mismatched keys silently no-op because curl ignores unknown config lines by default.
Step 6: Configure wget proxy via ~/.wgetrc
Wget respects http_proxy and https_proxy env vars, but only if use_proxy = on is set, either in the environment or in ~/.wgetrc. The cleanest pattern is to write both into a config file so wget behaves identically in interactive shells and in cron jobs. Create ~/.wgetrc:
cat > ~/.wgetrc <<EOF
use_proxy = yes
http_proxy = ${PROXY_URL}/
https_proxy = ${PROXY_URL}/
no_proxy = ${NO_PROXY_LIST}
EOF
chmod 600 ~/.wgetrc
Test wget end-to-end:
wget --tries=1 --timeout=5 https://example.com -O /dev/null 2>&1 | head -10
The first log line names the proxy wget resolved, which is the signal you want:
--2026-04-18 19:53:29-- https://example.com/
Resolving proxy.example.internal (proxy.example.internal)... failed: Name or service not known.
wget: unable to resolve host address 'proxy.example.internal'
With a real Squid, the third line becomes Connecting to proxy.example.internal:3128... connected. followed by the upstream request.
Step 7: Configure Git HTTP and HTTPS proxy
Git does not read the shell HTTP_PROXY variables for its smart-HTTP transport reliably across all versions, and its internal proxy setting wins anyway. Configure it explicitly with git config --global:
git config --global http.proxy "${PROXY_URL}"
git config --global https.proxy "${PROXY_URL}"
Confirm the keys landed in ~/.gitconfig:
git config --global --list | grep proxy
Expected output:
http.proxy=http://proxy.example.internal:3128
https.proxy=http://proxy.example.internal:3128
A shallow clone is the fastest test. With our placeholder proxy the proxy layer names the resolution failure, which proves git is trying the proxy and not going direct:
git clone --depth=1 https://github.com/git/git /tmp/git-test
On our test box:
Cloning into '/tmp/git-test'...
fatal: unable to access 'https://github.com/git/git/': Could not resolve proxy: proxy.example.internal
For one repo that should bypass the proxy (say an internal GitLab reachable without it), scope an override with http.<url>.proxy:
git config --global http.https://gitlab.internal/.proxy ""
An empty string disables the proxy for that URL prefix. SSH-based git URLs ([email protected]:...) ignore HTTP proxy settings entirely; they need their own solution, usually ProxyCommand in ~/.ssh/config pointing to nc or corkscrew.
Step 8: Configure Podman and Docker proxy
Podman reads proxy settings from /etc/containers/containers.conf, not from the shell environment of whoever runs podman pull. The key is an env list under [engine]:
sudo tee /etc/containers/containers.conf > /dev/null <<EOF
[engine]
env = [
"HTTP_PROXY=${PROXY_URL}",
"HTTPS_PROXY=${PROXY_URL}",
"NO_PROXY=${NO_PROXY_LIST}",
]
EOF
For rootless Podman (the default on Rocky 10), the same file at ~/.config/containers/containers.conf under your user account overrides the system file.
Test with a short pull that only reaches the registry handshake phase:
podman pull docker.io/library/alpine:latest 2>&1 | head -5
Podman names the proxy explicitly when it fails, so you can confirm the config file was picked up:
Trying to pull docker.io/library/alpine:latest...
Error: initializing source docker://alpine:latest: pinging container registry
registry-1.docker.io: Get "http://registry-1.docker.io/v2/": proxyconnect tcp:
dial tcp: lookup proxy.example.internal on 192.168.1.1:53: no such host
Docker works differently. The daemon runs as a systemd service and does not read user-level config. The correct pattern is a systemd drop-in at /etc/systemd/system/docker.service.d/http-proxy.conf:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf > /dev/null <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
Verify the daemon sees the new environment with systemctl show:
sudo systemctl show --property=Environment docker
The output should list all three HTTP_PROXY, HTTPS_PROXY, and NO_PROXY values. Full install walkthroughs for both engines are in Install Docker CE on Rocky Linux and Install Podman and Buildah on Rocky Linux.
Note that the daemon proxy only affects docker pull, docker push, and build steps that fetch base images. It does not affect traffic from inside running containers; for that, set the proxy inside the container itself (build args or docker run -e).
Step 9: Proxy for any systemd service
The Docker drop-in pattern generalizes. Any service that needs outbound HTTP (cloud-init, Ansible pull, a backup agent, dnf-automatic) takes the same treatment. Create a drop-in directory named after the unit, then a .conf file inside it:
SERVICE="my-agent"
sudo mkdir -p "/etc/systemd/system/${SERVICE}.service.d"
sudo tee "/etc/systemd/system/${SERVICE}.service.d/http-proxy.conf" > /dev/null <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
sudo systemctl daemon-reload
sudo systemctl restart "${SERVICE}"
Verify the service inherited the environment. On our test box the crond unit picked up the drop-in immediately:
systemctl show crond | grep -i environment | head -3
Expected:
Environment=HTTP_PROXY=http://proxy.example.internal:3128 HTTPS_PROXY=http://proxy.example.internal:3128 NO_PROXY=localhost,127.0.0.1,.example.com
EnvironmentFiles=/etc/sysconfig/crond (ignore_errors=no)
Dropping the file under /etc/systemd/system/ (not /usr/lib/systemd/system/) keeps the override separate from the vendor-shipped unit, so a package upgrade never overwrites it. Many production teams manage these drop-ins with Squid on one side and Ansible pushing the drop-in on the other.
Step 10: GNOME desktop proxy (optional)
If the Rocky, Alma, or Fedora box runs the GNOME desktop, the Settings UI has a Network Proxy panel: Settings > Network > Network Proxy. Set the mode to Manual, fill in the HTTP, HTTPS, and FTP fields with the same host and port, then close. GNOME writes to org.gnome.system.proxy via dconf, which covers GNOME-aware applications (GNOME Shell extensions, GNOME Web, some Flatpak apps).
GNOME’s panel does NOT touch DNF, curl, wget, systemd services, or Podman. On a server box those live in the files you edited in Steps 3 through 9. The desktop panel is a complement, not a replacement.
Step 11: Verify every layer sees the proxy
A single command that proves every layer of the stack is reading the proxy is useful during rollout and during incident recovery. Run this as a one-liner:
echo "=== Shell env ==="; env | grep -i proxy | sort
echo "=== DNF ==="; grep ^proxy /etc/dnf/dnf.conf
echo "=== curl ==="; grep -E '^proxy|^noproxy' ~/.curlrc 2>/dev/null
echo "=== wget ==="; grep -E '^(use_proxy|http_proxy|https_proxy)' ~/.wgetrc 2>/dev/null
echo "=== git ==="; git config --global --list | grep proxy
echo "=== Podman ==="; grep -A3 '\[engine\]' /etc/containers/containers.conf 2>/dev/null
Every section that is configured prints a value. Empty sections either have not been configured yet or have a typo in the config file. A useful smoke test on a live proxy is to time a curl -o /dev/null -s -w "%{http_code}\n" https://example.com; an HTTP 200 through the proxy means the full chain is green.
How to remove the proxy later
When the host moves to an environment with direct egress, the teardown is the inverse of the install. Remove the persistent files:
sudo rm -f /etc/profile.d/proxy.sh
sudo sed -i '/^proxy=/d; /^proxy_username=/d; /^proxy_password=/d' /etc/dnf/dnf.conf
rm -f ~/.curlrc ~/.wgetrc
git config --global --unset http.proxy
git config --global --unset https.proxy
sudo rm -f /etc/containers/containers.conf
sudo rm -rf /etc/systemd/system/docker.service.d
sudo systemctl daemon-reload
sudo systemctl restart docker 2>/dev/null || true
Drop out of the current shell and reconnect so the old HTTP_PROXY exports disappear, or clear them in place:
unset HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy
env | grep -i proxy
Empty output from the second command confirms the shell is clean. A curl -v https://example.com should now reach the upstream directly with no proxy mention in the verbose log.
Keep the box locked down while you’re at it
A proxy-gated host usually sits inside a firewall, so tightening local firewalld rules pays for itself. The RHEL-family firewalld primer covers service-based rules, zones, and rich rules. On the credentials side, storing the proxy password in a password manager rather than a plaintext dnf.conf is the right answer for any shared host. 1Password for Business handles this cleanly for a team; for a solo box, Tailscale plus an internal DNS name for the proxy sidesteps the credentials problem entirely by authenticating at the network layer.
Fresh Rocky 10 builds should run the Rocky 10 post-install checklist first; the SELinux, fail2ban, and unattended-upgrade steps in that guide are exactly the things that break quietly when a proxy gets in the middle, so knowing the default state helps you debug later.
Stuck on a sticky proxy rollout across dozens of RHEL-family boxes, or chasing down a TLS-inspecting proxy breaking your DNF mirror list? Our team runs RHEL/Rocky platform engagements regularly. Reach out with a short description of the setup and we will scope it.
# user name for authenticating to an http proxy, if needed
proxy_user =
should be proxy_username =
no?
Correct.
Edwin is correct; it needs to be “proxy_username=”.
Additionally, in the sentence right below the “Configure System-Wide Proxy settings on CLI” section, it says, “We will add a shell script file under /etc/profile.d/proxy.sh”. However, the command shown right afterwards is “sudo vi /etc/profile”. This causes confusion for readers when the command used references a different file than what was specified right before that.
This has been captured correctly in the article. Thanks for the insights
sorry could i ask you to fix?
Thank you so much for teaching. This article help me setup my Cent OS 7.
Thanks for the positive comment.
Hi Josphat, thanks for the article.
I have a few questions that I hope you can clarify.
On my Centos 7 / Rocky servers I have set PROXY_URL=ip_address:8080/ (without http before the ip) and I see that it works, the proxy processes the requests, so the http before the IP address is not imperative?
On Ubuntu I have verified that without http before the IP address it does not work, so you have to put it on.
Furthermore, I also see that both http and https traffic are proxed, even if I have not set the line: https_proxy=http://ip_address:8080/, is this normal behavior?
Many thanks in advance!