This guide is for deployments that do not fit the automated installer. Use it when AmneziaWG is managed by another tool, runs in Docker, runs on a different network namespace, or when you want to own the systemd, firewall, and config layout yourself.
The proxy is deliberately simple:
- it listens on a public UDP address (
listen); - it forwards valid AmneziaWG traffic to a private backend UDP address
(
backend); - it can read the AWG server config (
awg_config) to apply the same padding transformation that AmneziaWG expects; - it can write a local session status file (
status_file) foramneziawg-web.
The proxy does not need to manage the AWG interface. In a manual deployment, you only need to make sure the proxy can reach the AWG UDP backend and that clients connect to the proxy's public UDP port.
This is the simplest and safest layout.
client -> server public IP:51820/udp -> amneziawg-proxy
amneziawg-proxy -> 127.0.0.1:51821/udp -> AWG interface
Configure AWG to listen only on loopback and a backend port:
[Interface]
ListenAddr = 127.0.0.1
ListenPort = 51821Then configure the proxy:
listen = "0.0.0.0:51820"
backend = "127.0.0.1:51821"
imitate_protocol = "quic"
awg_config = "/etc/amnezia/amneziawg/awg0.conf"
status_file = "/var/lib/amneziawg-proxy/sessions.json"
status_interval_secs = 5Only the proxy port should be reachable from the internet.
This layout is useful when an existing container owns the AWG interface. Publish the container's AWG UDP port to a host loopback-only backend port:
docker run \
--name awg \
--cap-add NET_ADMIN \
--cap-add SYS_MODULE \
-p 127.0.0.1:51821:51820/udp \
your-awg-imageThen set:
listen = "0.0.0.0:51820"
backend = "127.0.0.1:51821"
imitate_protocol = "quic"If the AWG config is available on the host, mount or copy it read-only and set
awg_config:
awg_config = "/etc/amnezia/amneziawg/awg0.conf"If the host cannot read the AWG config, omit awg_config. The proxy will still
forward traffic, but it will not load AWG obfuscation parameters, classify AWG
packet types, or apply padding transformation. Use this only if your AWG setup
does not require that transformation or if you have verified clients still
connect correctly.
Put both containers on the same Docker network. Because backend currently
expects an IP socket address, not a DNS name, use a static container IP or
publish AWG to a loopback port on the Docker host.
Example with static bridge-network IPs:
services:
awg:
image: your-awg-image
cap_add:
- NET_ADMIN
- SYS_MODULE
networks:
awg_net:
ipv4_address: 172.30.0.10
expose:
- "51820/udp"
proxy:
image: your-amneziawg-proxy-image
command: ["/usr/local/bin/amneziawg-proxy", "/etc/amneziawg-proxy/proxy.toml"]
ports:
- "51820:51820/udp"
volumes:
- ./proxy.toml:/etc/amneziawg-proxy/proxy.toml:ro
- ./sessions:/var/lib/amneziawg-proxy
# Optional, only if available:
- ./awg0.conf:/etc/amnezia/amneziawg/awg0.conf:ro
networks:
awg_net:
ipv4_address: 172.30.0.20
networks:
awg_net:
ipam:
config:
- subnet: 172.30.0.0/24proxy.toml:
listen = "0.0.0.0:51820"
backend = "172.30.0.10:51820"
imitate_protocol = "quic"
awg_config = "/etc/amnezia/amneziawg/awg0.conf"
status_file = "/var/lib/amneziawg-proxy/sessions.json"
status_interval_secs = 5Do not expose the AWG container port directly to the internet in this layout. Expose only the proxy port.
This is supported, but treat the backend path as trusted internal network traffic:
client -> proxy public IP:51820/udp -> proxy
proxy -> private network IP:51821/udp -> AWG host
Example:
listen = "0.0.0.0:51820"
backend = "10.10.0.5:51821"
imitate_protocol = "quic"Make sure the AWG backend address is reachable only from the proxy host, for example with a firewall rule on the AWG host.
From the repository root:
cd amneziawg-proxy
cargo build --release
sudo install -m 0755 target/release/amneziawg-proxy /usr/local/bin/amneziawg-proxyCreate config and data directories:
sudo install -d -m 0755 /etc/amneziawg-proxy
sudo install -d -m 0750 /var/lib/amneziawg-proxyIf amneziawg-web runs on the same host and should read proxy sessions, allow
its service group to traverse the proxy data directory:
sudo chown root:awg-web /var/lib/amneziawg-proxy
sudo chmod 2750 /var/lib/amneziawg-proxyIf amneziawg-web is not installed, keep the directory owned by root:root.
Minimal same-host example:
listen = "0.0.0.0:51820"
backend = "127.0.0.1:51821"
imitate_protocol = "quic"
session_ttl_secs = 300
rate_limit_per_sec = 5
# Aggregate ceiling on reply bytes/sec to unauthenticated traffic across all
# sources. Unlike rate_limit_per_sec above, this is not keyed by source, so
# spoofing cannot reset it. This is the amplification control.
probe_reply_bytes_per_sec = 32768
status_file = "/var/lib/amneziawg-proxy/sessions.json"
status_interval_secs = 5
awg_config = "/etc/amnezia/amneziawg/awg0.conf"Important notes:
listenis the public UDP endpoint that client configs should use.backendis the real AWG UDP listener. Keep it private.backendmust be an IP address and port, for example127.0.0.1:51821or172.30.0.10:51820. Hostnames such as Docker service names are not accepted.awg_configis optional, but recommended when the proxy must apply AWG padding transformation. The file only needs the[Interface]obfuscation parameters; peer sections are ignored.status_filedefaults to/var/lib/amneziawg-proxy/sessions.jsonwhen it is omitted. Set it explicitly whenamneziawg-webshould read sessions from a different path.
Install the config:
sudo tee /etc/amneziawg-proxy/proxy.toml > /dev/null <<'EOF'
listen = "0.0.0.0:51820"
backend = "127.0.0.1:51821"
imitate_protocol = "quic"
session_ttl_secs = 300
rate_limit_per_sec = 5
# Aggregate ceiling on reply bytes/sec to unauthenticated traffic across all
# sources. Unlike rate_limit_per_sec above, this is not keyed by source, so
# spoofing cannot reset it. This is the amplification control.
probe_reply_bytes_per_sec = 32768
status_file = "/var/lib/amneziawg-proxy/sessions.json"
status_interval_secs = 5
awg_config = "/etc/amnezia/amneziawg/awg0.conf"
EOF
sudo chmod 0644 /etc/amneziawg-proxy/proxy.tomlThe public client endpoint should point to the proxy. The AWG backend should listen on a private address.
For same-host AWG, edit the AWG interface config:
[Interface]
ListenAddr = 127.0.0.1
ListenPort = 51821Reload the interface using the command appropriate for your deployment. On a standard host installation, that is usually:
sudo wg syncconf awg0 <(sudo wg-quick strip awg0)For Docker-based AWG, update the container configuration instead. A common
pattern is to leave AWG listening on 51820/udp inside the container, but
publish it to the host as 127.0.0.1:51821/udp.
Create /etc/systemd/system/amneziawg-proxy.service:
[Unit]
Description=AmneziaWG UDP Proxy
After=network.target
[Service]
Type=simple
Restart=on-failure
RestartSec=5s
LimitNOFILE=16384
UMask=0027
User=root
ExecStart=/usr/local/bin/amneziawg-proxy /etc/amneziawg-proxy/proxy.toml
WorkingDirectory=/var/lib/amneziawg-proxy
Environment=RUST_LOG=amneziawg_proxy=info
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ReadOnlyPaths=-/etc/amnezia /etc/amneziawg-proxy
ReadWritePaths=/var/lib/amneziawg-proxy
[Install]
WantedBy=multi-user.targetIf awg_config points somewhere else, make sure the service user has normal
filesystem read access to that file. You can also add the config directory to
ReadOnlyPaths for hardening; prefix it with - when the directory may be
absent on hosts where awg_config is omitted.
For public ports below 1024, either keep User=root or add
AmbientCapabilities=CAP_NET_BIND_SERVICE and run as a dedicated service user
that can read proxy.toml, read awg_config if configured, and write
status_file.
Start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now amneziawg-proxy
sudo systemctl status amneziawg-proxy- Allow inbound UDP to the proxy public port, for example
51820/udp. - Do not expose the AWG backend port to the internet.
- If the backend is on another host, allow the backend UDP port only from the proxy host.
- If Docker is used, verify published ports. Prefer
127.0.0.1:<backend-port>:<container-awg-port>/udpfor the backend.
Example ufw rules for same-host deployment:
sudo ufw allow 51820/udp
sudo ufw deny 51821/udpamneziawg-web can show active proxy sessions when it can read the status file.
Use the same path in both services:
Proxy proxy.toml:
status_file = "/var/lib/amneziawg-proxy/sessions.json"
status_interval_secs = 5Web environment:
AWG_PROXY_SESSIONS_FILE=/var/lib/amneziawg-proxy/sessions.jsonWhen both services are on the same host, the proxy data directory must be readable by the web service group:
sudo chown root:awg-web /var/lib/amneziawg-proxy
sudo chmod 2750 /var/lib/amneziawg-proxyIf the proxy runs in Docker, mount the status directory on the host and point
AWG_PROXY_SESSIONS_FILE at the mounted sessions.json. If the proxy is not
installed or the file is unavailable, the web panel hides the proxy sessions
section and continues to work normally.
Check that the proxy starts:
sudo journalctl -u amneziawg-proxy -n 100 --no-pagerCheck that it is listening:
sudo ss -lunp | grep amneziawg-proxyCheck that the AWG backend is not publicly exposed:
sudo ss -lunp | grep -E '51820|51821'After a client connects, check the session status file:
sudo cat /var/lib/amneziawg-proxy/sessions.jsonFor Docker-hosted AWG, also verify that the proxy can reach the container's UDP
port and that the container is not publishing the backend port on 0.0.0.0.
- Confirm client configs point to the proxy public address and port.
- Confirm
backendpoints to the real AWG listener. - Confirm AWG is listening on the backend port.
- Confirm the backend firewall allows traffic from the proxy.
- If
awg_configis omitted, verify that this deployment works without proxy padding transformation.
AWG and the proxy cannot both bind the same host port. Move AWG to a backend port or bind it only inside a container/private namespace, then let the proxy own the public port.
backend must be an IP socket address. Use a static Docker network IP, publish
the AWG container to a host loopback port, or run the proxy in a mode where the
backend has a stable IP address.
- Confirm
status_fileandAWG_PROXY_SESSIONS_FILEare the same path. - Confirm the proxy has written
sessions.json. - Confirm the web service user can traverse the status directory and read the file.
- Confirm
ReadOnlyPathsin the web systemd unit includes the status directory whenProtectSystem=strictis enabled.