Envoy is a high-performance, open-source Layer 7 proxy and communication bus designed for large modern service-oriented architectures. Originally built at Lyft and now a graduated CNCF project, Envoy is the data plane behind Istio and most major service mesh implementations. It handles load balancing, observability, TLS termination, HTTP/2, gRPC proxying, circuit breaking, and much more – all configured through a flexible YAML-based or xDS API-driven model.

In this guide, we walk through installing and configuring Envoy Proxy on RHEL 10, Rocky Linux 10, and AlmaLinux 10. We cover both RPM-based installation and Docker deployment, build a working reverse proxy configuration from scratch, enable the admin interface, set up access logging, configure TLS termination, and cover basic rate limiting.

Prerequisites

Before you begin, make sure you have:

  • A running RHEL 10, Rocky Linux 10, or AlmaLinux 10 server
  • Root or sudo access
  • A stable internet connection for downloading packages
  • Basic familiarity with YAML syntax and Linux networking
  • SELinux in enforcing or permissive mode (we cover adjustments where needed)
  • Firewall access to configure required ports

Update your system packages before proceeding:

sudo dnf update -y

Verify your OS release:

cat /etc/redhat-release

You should see output similar to:

Rocky Linux release 10.0 (Obsidian Ocelot)

Method 1 – Install Envoy Proxy via Official RPM Repository

The Envoy project provides official RPM packages through the GetEnvoy repository maintained by Tetrate. This is the most straightforward way to get Envoy running on RHEL-based systems.

Step 1 – Add the Envoy RPM Repository

Create the Envoy yum repository file:

sudo tee /etc/yum.repos.d/envoy.repo <<'EOF'
[envoy]
name=Envoy Proxy
baseurl=https://rpm.dl.getenvoy.io/public/rpm/el/$releasever/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://rpm.dl.getenvoy.io/public/gpg.8115BA8E629CC074.key
sslverify=1
EOF

Verify the repository was added:

sudo dnf repolist | grep envoy

Step 2 – Install Envoy

Install the Envoy package:

sudo dnf install -y getenvoy-envoy

If the GetEnvoy repository does not yet carry packages for RHEL 10, you can download the official Envoy binary directly from the project’s GitHub releases page as an alternative:

# Download the latest Envoy binary from func-e (Envoy distribution tool)
curl -L https://func-e.io/install.sh | bash -s -- -b /usr/local/bin
func-e use 1.32.3
sudo cp ~/.func-e/versions/1.32.3/bin/envoy /usr/local/bin/envoy

Verify the installation:

envoy --version

Expected output:

envoy  version: 1.32.3/Clean/RELEASE/BoringSSL

Method 2 – Install Envoy Proxy via Docker

Running Envoy in a container is the most portable and commonly used approach in production environments, especially when you are already using container orchestration.

Step 1 – Install Docker or Podman

On RHEL 10 and its derivatives, Podman is available out of the box. You can use either Podman or Docker:

# Using Podman (available by default)
sudo dnf install -y podman

# Or install Docker CE
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io
sudo systemctl enable --now docker

Verify your container runtime:

podman --version
# or
docker --version

Step 2 – Pull the Envoy Image

docker pull envoyproxy/envoy:v1.32-latest

Verify the image:

docker run --rm envoyproxy/envoy:v1.32-latest --version

Step 3 – Run Envoy in a Container

Once we build a configuration file (covered in the next section), you can run Envoy in Docker like this:

docker run -d \
  --name envoy \
  --restart unless-stopped \
  -p 10000:10000 \
  -p 9901:9901 \
  -v /etc/envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro \
  envoyproxy/envoy:v1.32-latest \
  -c /etc/envoy/envoy.yaml

Verify the container is running:

docker ps | grep envoy

Configure Envoy from Scratch – envoy.yaml

Envoy uses a YAML configuration file that defines listeners, clusters, routes, and endpoints. Understanding these core concepts is important before writing any configuration.

Core Concepts

  • Listener – A named network location (IP + port) where Envoy accepts incoming connections
  • Filter Chain – A set of network filters applied to connections on a listener
  • Route – Rules that match incoming requests and direct them to a cluster
  • Cluster – A group of upstream hosts (endpoints) that Envoy can proxy traffic to
  • Endpoint – An individual backend server address within a cluster

Create the Configuration Directory

sudo mkdir -p /etc/envoy

Build the Static Configuration

Create the main Envoy configuration file. This example sets up a reverse proxy that listens on port 10000 and forwards traffic to a backend application running on port 8080:

sudo tee /etc/envoy/envoy.yaml <<'EOF'
admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 9901

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_cluster

  clusters:
    - name: backend_cluster
      type: STRICT_DNS
      connect_timeout: 5s
      load_assignment:
        cluster_name: backend_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 8080
EOF

Validate the configuration:

envoy --mode validate -c /etc/envoy/envoy.yaml

You should see:

configuration '/etc/envoy/envoy.yaml' OK

Understanding the Configuration

Let’s break down what each section does:

  • admin – Exposes the Envoy admin interface on localhost port 9901 for stats, config dumps, and health checks
  • listeners – Defines a single listener on all interfaces (0.0.0.0) at port 10000
  • filter_chains – Applies the HTTP connection manager filter, which handles HTTP/1.1 and HTTP/2 traffic
  • route_config – Routes all incoming requests (prefix “/”) to the backend_cluster
  • clusters – Defines the backend_cluster pointing to 127.0.0.1:8080 with a 5-second connection timeout

Create a systemd Service Unit for Envoy

For RPM-based installations, create a systemd service to manage the Envoy process:

Step 1 – Create the Envoy System User

sudo useradd --system --no-create-home --shell /usr/sbin/nologin envoy

Step 2 – Set File Permissions

sudo chown -R envoy:envoy /etc/envoy
sudo mkdir -p /var/log/envoy
sudo chown -R envoy:envoy /var/log/envoy

Step 3 – Create the Service Unit File

sudo tee /etc/systemd/system/envoy.service <<'EOF'
[Unit]
Description=Envoy Proxy
Documentation=https://www.envoyproxy.io/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=envoy
Group=envoy
ExecStart=/usr/local/bin/envoy -c /etc/envoy/envoy.yaml --log-level info
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

Step 4 – Enable and Start Envoy

sudo systemctl daemon-reload
sudo systemctl enable --now envoy

Verify the service is running:

sudo systemctl status envoy

Expected output should show “active (running)”:

● envoy.service - Envoy Proxy
     Loaded: loaded (/etc/systemd/system/envoy.service; enabled; preset: disabled)
     Active: active (running) since ...

Configure the Firewall

Open the required ports in firewalld:

sudo firewall-cmd --permanent --add-port=10000/tcp
sudo firewall-cmd --permanent --add-port=9901/tcp
sudo firewall-cmd --reload

Verify the ports are open:

sudo firewall-cmd --list-ports

Test Envoy with curl

First, start a simple backend to test against. Python’s built-in HTTP server works well for this:

# Start a test backend on port 8080 in the background
mkdir -p /tmp/envoy-test
echo "Hello from backend" > /tmp/envoy-test/index.html
cd /tmp/envoy-test && python3 -m http.server 8080 &

Now test the Envoy proxy on port 10000:

curl -v http://localhost:10000/

You should see the response from the backend with Envoy-specific headers like x-envoy-upstream-service-time in the response:

< HTTP/1.1 200 OK
< server: envoy
< x-envoy-upstream-service-time: 1
...
Hello from backend

Admin Interface on Port 9901

The admin interface gives you real-time visibility into Envoy's state. Since we bound it to 127.0.0.1, it is only accessible from the local machine.

curl http://localhost:9901/

This returns an HTML page listing all available admin endpoints. Some useful ones:

# View cluster health status
curl http://localhost:9901/clusters

# View server stats
curl http://localhost:9901/stats

# View Envoy server info
curl http://localhost:9901/server_info

# Dump the running configuration
curl http://localhost:9901/config_dump

# View listener status
curl http://localhost:9901/listeners

# Check readiness
curl http://localhost:9901/ready

Verify the admin interface returns a 200 status:

curl -s -o /dev/null -w "%{http_code}" http://localhost:9901/ready

Expected output:

200

Security note: Never expose the admin interface to public networks. If you need remote access, use SSH tunneling:

ssh -L 9901:127.0.0.1:9901 user@your-server

Configure Access Logging

The basic configuration above sends access logs to stdout. For production, you typically want logs written to a file with a structured format.

Replace the access_log section in your envoy.yaml with:

                access_log:
                  - name: envoy.access_loggers.file
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                      path: /var/log/envoy/access.log
                      log_format:
                        json_format:
                          timestamp: "%START_TIME%"
                          method: "%REQ(:METHOD)%"
                          path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                          protocol: "%PROTOCOL%"
                          response_code: "%RESPONSE_CODE%"
                          response_flags: "%RESPONSE_FLAGS%"
                          upstream_host: "%UPSTREAM_HOST%"
                          duration: "%DURATION%"
                          request_id: "%REQ(X-REQUEST-ID)%"
                          user_agent: "%REQ(USER-AGENT)%"

Restart Envoy to apply the changes:

sudo systemctl restart envoy

Verify logs are being written:

curl http://localhost:10000/
tail -1 /var/log/envoy/access.log | python3 -m json.tool

You should see a formatted JSON log entry with all the fields defined in your configuration.

TLS Termination

Envoy can terminate TLS connections so your backend services do not need to handle certificates. This section shows how to configure HTTPS on the listener.

Step 1 - Prepare TLS Certificates

If you have certificates from a CA, place them in the Envoy configuration directory. For testing, generate a self-signed certificate:

sudo mkdir -p /etc/envoy/certs
sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /etc/envoy/certs/server.key \
  -out /etc/envoy/certs/server.crt \
  -subj "/CN=envoy.example.com"
sudo chown -R envoy:envoy /etc/envoy/certs
sudo chmod 600 /etc/envoy/certs/server.key

Verify the certificate:

openssl x509 -in /etc/envoy/certs/server.crt -text -noout | head -15

Step 2 - Update the Listener for TLS

Create a new listener configuration with TLS support. Add an HTTPS listener on port 8443 to your envoy.yaml:

  listeners:
    - name: listener_https
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8443
      filter_chains:
        - transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/certs/server.crt
                    private_key:
                      filename: /etc/envoy/certs/server.key
                alpn_protocols:
                  - h2
                  - http/1.1
          filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_https
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: https_route
                  virtual_hosts:
                    - name: backend_service_tls
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_cluster

Restart Envoy and open the firewall port:

sudo firewall-cmd --permanent --add-port=8443/tcp
sudo firewall-cmd --reload
sudo systemctl restart envoy

Test the HTTPS endpoint:

curl -k -v https://localhost:8443/

You should see a successful TLS handshake and the backend response.

Rate Limiting Basics

Envoy supports local rate limiting without requiring an external rate limit service. This is useful for protecting backends from excessive traffic.

Add the local rate limit filter to your HTTP filter chain, placing it before the router filter:

                http_filters:
                  - name: envoy.filters.http.local_ratelimit
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
                      stat_prefix: http_local_rate_limiter
                      token_bucket:
                        max_tokens: 100
                        tokens_per_fill: 100
                        fill_interval: 60s
                      filter_enabled:
                        runtime_key: local_rate_limit_enabled
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                      filter_enforced:
                        runtime_key: local_rate_limit_enforced
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                      response_headers_to_add:
                        - append_action: OVERWRITE_IF_EXISTS_OR_ADD
                          header:
                            key: x-local-rate-limit
                            value: "true"
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

This configuration allows 100 requests per 60-second window. When the limit is exceeded, Envoy returns a 429 (Too Many Requests) response.

After applying the configuration, restart Envoy and test:

sudo systemctl restart envoy

# Send multiple rapid requests to test rate limiting
for i in $(seq 1 110); do
  curl -s -o /dev/null -w "%{http_code}\n" http://localhost:10000/
done | sort | uniq -c

You should see 100 responses with code 200 and some with code 429.

Complete Production Configuration Example

Here is a full envoy.yaml that combines everything covered above - HTTP and HTTPS listeners, file-based access logging, local rate limiting, and the admin interface:

admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 9901

static_resources:
  listeners:
    # HTTP listener on port 10000
    - name: listener_http
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_log:
                  - name: envoy.access_loggers.file
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                      path: /var/log/envoy/access.log
                      log_format:
                        json_format:
                          timestamp: "%START_TIME%"
                          method: "%REQ(:METHOD)%"
                          path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                          protocol: "%PROTOCOL%"
                          response_code: "%RESPONSE_CODE%"
                          duration: "%DURATION%"
                          upstream_host: "%UPSTREAM_HOST%"
                http_filters:
                  - name: envoy.filters.http.local_ratelimit
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
                      stat_prefix: http_local_rate_limiter
                      token_bucket:
                        max_tokens: 100
                        tokens_per_fill: 100
                        fill_interval: 60s
                      filter_enabled:
                        runtime_key: local_rate_limit_enabled
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                      filter_enforced:
                        runtime_key: local_rate_limit_enforced
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_cluster

    # HTTPS listener on port 8443
    - name: listener_https
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8443
      filter_chains:
        - transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/certs/server.crt
                    private_key:
                      filename: /etc/envoy/certs/server.key
                alpn_protocols:
                  - h2
                  - http/1.1
          filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_https
                access_log:
                  - name: envoy.access_loggers.file
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                      path: /var/log/envoy/access.log
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: https_route
                  virtual_hosts:
                    - name: backend_service_tls
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_cluster

  clusters:
    - name: backend_cluster
      type: STRICT_DNS
      connect_timeout: 5s
      load_assignment:
        cluster_name: backend_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 8080

Troubleshooting

Envoy Fails to Start

Check the logs for configuration errors:

sudo journalctl -u envoy -e --no-pager

Validate the configuration before restarting:

envoy --mode validate -c /etc/envoy/envoy.yaml

Port Already in Use

Check which process is using the port:

sudo ss -tlnp | grep 10000

Connection Refused to Backend

Verify the backend is running and accessible:

curl http://127.0.0.1:8080/

Check Envoy cluster health status:

curl http://localhost:9901/clusters | grep backend_cluster

Look for "health_flags" in the output. A healthy endpoint shows ::health_flags::healthy.

SELinux Blocking Envoy

If Envoy cannot bind to ports or read configuration files, SELinux may be the cause:

# Check for SELinux denials
sudo ausearch -m avc -ts recent

# Allow Envoy to bind to non-standard ports
sudo setsebool -P httpd_can_network_connect 1

# If needed, create a custom SELinux policy
sudo ausearch -c 'envoy' --raw | audit2allow -M envoy-custom
sudo semodule -i envoy-custom.pp

TLS Certificate Issues

Verify the certificate and key match:

openssl x509 -noout -modulus -in /etc/envoy/certs/server.crt | openssl md5
openssl rsa -noout -modulus -in /etc/envoy/certs/server.key | openssl md5

Both commands should return the same MD5 hash. If they differ, the certificate and key do not match.

High Memory or CPU Usage

Check Envoy stats for connection and request counts:

curl -s http://localhost:9901/stats | grep -E "downstream_cx_active|downstream_rq_active"

Review the memory allocation stats:

curl -s http://localhost:9901/memory

DNS Resolution Failures

If your cluster uses STRICT_DNS and the upstream hostname cannot be resolved:

# Check DNS from the server
dig your-backend-hostname

# Check Envoy DNS stats
curl -s http://localhost:9901/stats | grep dns

Consider switching the cluster type to STATIC and using IP addresses directly if DNS is unreliable.

Conclusion

You now have Envoy Proxy installed and configured on RHEL 10, Rocky Linux 10, or AlmaLinux 10. The setup covers a working reverse proxy with static configuration, systemd service management, the admin interface for monitoring, JSON access logging, TLS termination, and local rate limiting. From here you can expand your configuration with additional clusters, health checking, circuit breakers, or integrate with an xDS control plane for dynamic configuration management.

LEAVE A REPLY

Please enter your comment!
Please enter your name here