Fix WireGuard wg Command I/O Issues in an AWS SSM Session Manager Session

I installed WireGuard on an Ubuntu 26.04 EC2 instance and connected to the instance through an AWS SSM Session Manager session.

After installing WireGuard, I first noticed the issue when I ran:

wg --version

The command appeared to print nothing.

At first, I thought this was only a wg --version problem. However, after debugging it further, the real issue was broader.

The WireGuard wg command had I/O issues inside the SSM Session Manager session. Commands that depended on inherited stdin, stdout, stderr, or redirected files outside the paths allowed by the wg AppArmor profile could fail.

The same command worked correctly on my local Ubuntu 26.04 VM.

The real cause was AppArmor.

More specifically, the issue was caused by the interaction between:

AWS SSM Session Manager
Snap-installed amazon-ssm-agent
AppArmor confinement
Ubuntu 26.04's /usr/bin/wg AppArmor profile

Environment

In this example, I used:

Cloud: AWS EC2
OS: Ubuntu 26.04
Login method: AWS SSM Session Manager
Package: wireguard-tools
Command: wg

The installed WireGuard userspace tool was:

wireguard-tools v1.0.20250521

The First Symptom

Inside the SSM Session Manager session, this command appeared to print nothing:

wg --version

Redirecting stdout and stderr to files also produced empty files:

wg --version > /tmp/wg.stdout 2> /tmp/wg.stderr

wc -c /tmp/wg.stdout /tmp/wg.stderr

Output:

0 /tmp/wg.stdout
0 /tmp/wg.stderr
0 total

However, this worked:

wg --version 2>&1 | cat

Output:

wireguard-tools v1.0.20250521 - https://git.zx2c4.com/wireguard-tools/

So wg was not completely broken. The command could still print output in some situations.

That meant the issue was probably related to file descriptors, redirection, or the execution environment.


Confirm the Real wg Binary

First, I checked whether wg was really /usr/bin/wg:

command -v wg
readlink -f "$(command -v wg)"
ls -l "$(command -v wg)"
file "$(command -v wg)"
dpkg -S "$(command -v wg)"

Example output:

/usr/bin/wg
/usr/bin/wg
-rwxr-xr-x 1 root root 134344 Nov 20 15:00 /usr/bin/wg
/usr/bin/wg: ELF 64-bit LSB pie executable, ARM aarch64
wireguard-tools: /usr/bin/wg

This confirmed that wg was the real binary from the wireguard-tools package.

It was not an alias, shell function, or wrapper script.


Debug with strace

Next, I used strace to see whether wg actually tried to write output.

strace -f -e trace=write,openat,exit_group \
  -o /tmp/trace_direct.txt \
  /usr/bin/wg --version > /tmp/test_direct.stdout

Then I checked the trace file:

cat /tmp/trace_direct.txt

The important line was:

write(1, "wireguard-tools v1.0.20250521 - "..., 71) = -1 EBADF (Bad file descriptor)

This means wg did try to write to file descriptor 1.

File descriptor 1 is stdout.

But stdout was invalid inside the wg process.

So the command was not silent because WireGuard did not print anything. It was silent because stdout was already broken when wg tried to write to it.

When I used a pipe, it worked:

strace -f -e trace=write,openat,exit_group \
  -o /tmp/trace_pipe.txt \
  /usr/bin/wg --version 2>&1 | cat

The trace showed:

write(1, "wireguard-tools v1.0.20250521 - "..., 71) = 71

So the difference was:

Direct stdout/file redirection: write(1) = EBADF
Pipe: write(1) = 71

This was the first important clue.


Normal Shell Redirection Still Worked

To make sure SSM Session Manager was not breaking all shell redirection, I tested normal commands:

/bin/echo "hello echo" > /tmp/echo.stdout
/usr/bin/printf "hello printf\n" > /tmp/printf.stdout
/bin/date > /tmp/date.stdout

wc -c /tmp/echo.stdout /tmp/printf.stdout /tmp/date.stdout
cat -A /tmp/echo.stdout
cat -A /tmp/printf.stdout
cat -A /tmp/date.stdout

These commands worked normally.

So the issue was not /tmp, not the shell, and not redirection in general.

The issue was specific to /usr/bin/wg.


Copying wg to /tmp Worked

Then I copied the same binary to /tmp:

cp /usr/bin/wg /tmp/wg.copy
chmod 755 /tmp/wg.copy

/tmp/wg.copy --version > /tmp/wg.copy.out 2> /tmp/wg.copy.err

wc -c /tmp/wg.copy.out /tmp/wg.copy.err
cat -A /tmp/wg.copy.out
cat -A /tmp/wg.copy.err

Output:

71 /tmp/wg.copy.out
 0 /tmp/wg.copy.err
71 total
wireguard-tools v1.0.20250521 - https://git.zx2c4.com/wireguard-tools/

This was a major clue.

The same executable content worked when copied to another path.

That strongly suggested a path-based security policy.


The Root Cause: AppArmor

I checked the current AppArmor confinement context:

cat /proc/$$/attr/current

Output:

snap.amazon-ssm-agent.amazon-ssm-agent (complain)

This means the SSM shell was not a normal unconfined shell. It was started by the Snap version of amazon-ssm-agent, and it was running under an AppArmor profile.

Then I checked the AppArmor profiles:

sudo aa-status | grep -iE 'wg|wireguard|ssm|amazon|session|profile|enforce' || true

The system had WireGuard-related AppArmor profiles loaded:

wg
wg-quick
wg-quick//ip
wg-quick//nft
wg-quick//sysctl

Then I checked the wg profile:

sudo sed -n '1,220p' /etc/apparmor.d/wg

The relevant part was:

profile wg /usr/bin/wg flags=(attach_disconnected, complain) {
  include <abstractions/base>
  include <abstractions/nameservice-strict>

  capability net_admin,
  capability net_bind_service,

  network netlink raw,
  network inet dgram,
  network inet6 dgram,
  network inet stream,
  network inet6 stream,

  file rw @{etc_rw}/wireguard/{,**},

  mr @{exec_path},

  include if exists <local/wg>
}

The complain flag appeared there because I had already switched the profile to complain mode during testing.

The important part is this rule:

file rw @{etc_rw}/wireguard/{,**},

That allows WireGuard files under:

/etc/wireguard/

But the profile did not allow inherited access to paths such as:

/tmp/*
/dev/pts/*
/dev/tty

That matters because in an SSM Session Manager session, stdout and stderr are connected to a pseudo-terminal such as /dev/pts/0.

If I redirect output to files, the inherited stdout/stderr file descriptors may point to files under /tmp.


Kernel Logs Confirmed the Problem

The kernel logs confirmed the AppArmor denial:

sudo journalctl -k --no-pager | grep -iE 'apparmor|DENIED|wg'

Important lines included:

apparmor="DENIED" operation="file_inherit" class="file" profile="wg" name="/tmp/wg.stdout" requested_mask="w" denied_mask="w"
apparmor="DENIED" operation="file_inherit" class="file" profile="wg" name="/tmp/wg.stderr" requested_mask="w" denied_mask="w"

There were also denials for the pseudo-terminal:

apparmor="DENIED" operation="file_inherit" class="file" profile="wg" name="/dev/pts/0" requested_mask="wr" denied_mask="wr"

The logs also showed denials for temporary key files such as:

/tmp/ec2_private.key
/tmp/ec2_public.key

So the issue was broader than wg --version.

The real issue was that the wg AppArmor profile denied inherited file descriptors when /usr/bin/wg was executed from the SSM Session Manager environment. The debug evidence also showed that changing the wg profile to complain mode allowed /usr/bin/wg --version > /tmp/wg.stdout to write 71 bytes successfully, and the profile itself allowed /etc/wireguard/**.

The flow was:

SSM Session Manager shell
→ executes /usr/bin/wg
→ AppArmor transitions the process into the wg profile
→ the wg profile denies inherited stdin/stdout/stderr or redirected files
→ fd 1 becomes invalid inside wg
→ wg tries write(1)
→ the kernel returns EBADF

That is why wg --version appeared to print nothing.


Confirming the Cause with aa-complain

To prove that AppArmor was the cause, I installed the AppArmor utilities:

sudo apt update
sudo apt install -y apparmor-utils

Then I switched the wg profile to complain mode:

sudo aa-complain /etc/apparmor.d/wg

After that, I tested the same command again:

rm -f /tmp/wg.stdout /tmp/wg.stderr

/usr/bin/wg --version > /tmp/wg.stdout 2> /tmp/wg.stderr

wc -c /tmp/wg.stdout /tmp/wg.stderr
cat -A /tmp/wg.stdout
cat -A /tmp/wg.stderr

Output:

71 /tmp/wg.stdout
 0 /tmp/wg.stderr
71 total
wireguard-tools v1.0.20250521 - https://git.zx2c4.com/wireguard-tools/

This confirmed the cause.

When the wg profile was enforced, AppArmor denied file_inherit.

When the wg profile was switched to complain mode, the same command worked.

After testing, switch the profile back to enforce mode:

sudo aa-enforce /etc/apparmor.d/wg

Why It Worked on My Local Ubuntu 26.04 VM

My local Ubuntu 26.04 VM used a normal console or SSH shell.

The EC2 instance was different because I connected through AWS SSM Session Manager.

In this case, the shell was running under:

snap.amazon-ssm-agent.amazon-ssm-agent

Then, when that shell executed /usr/bin/wg, AppArmor transitioned the process into the wg profile.

The local VM did not have the same SSM Snap AppArmor confinement chain, so wg worked normally there.


Quick Workaround for Displaying wg Output

wg --version 2>&1 | cat

This works because stdout becomes a pipe, and wg can write to it successfully.

However, this is only a workaround for displaying output.

It is not the clean fix for configuring WireGuard.


Correct Fix: Keep WireGuard Files Under /etc/wireguard

The clean fix for WireGuard key and configuration files is to use the path that the Ubuntu wg AppArmor profile already allows:

/etc/wireguard/

Do not generate WireGuard keys under /tmp when the wg AppArmor profile is in enforce mode.

Avoid this:

wg genkey > /tmp/ec2_private.key
wg pubkey < /tmp/ec2_private.key > /tmp/ec2_public.key

The AppArmor logs showed denials for files under /tmp, including key files.

Instead, create the WireGuard directory:

sudo install -d -m 700 /etc/wireguard

Generate the private key directly under /etc/wireguard/:

sudo sh -c 'umask 077; wg genkey > /etc/wireguard/ec2_private.key'

Generate the public key:

sudo sh -c 'wg pubkey < /etc/wireguard/ec2_private.key > /etc/wireguard/ec2_public.key'

Print the public key:

sudo cat /etc/wireguard/ec2_public.key

This keeps WireGuard files in the path expected by the AppArmor profile.

Did this guide save you time?

Support this site

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top