Configure WireGuard via AWS SSM Parameter Store

A previous article covered how to run WireGuard on AWS ECS (Elastic Container Service), using an AWS EFS (Elastic File Server) volume as the source for the container’s WireGuard configuration. This is the simplest way to source an ECS container’s WireGuard config, but by no means the only way. This article will show you how to set up a custom WireGuard container image that will pull its config from the AWS SSM (Systems Manager) Parameter Store.

In this article, we’ll use our example WireGuard containers to allow point-to-point access to individual ECS hosts, where we’ll configure our WireGuard containers to allow access to the other containers on the same host:

Point to Point WireGuard on AWS ECS with SSM Parameter Store

Using this approach, you can use the same container image and task definition to launch multiple different WireGuard containers with different configuration settings, each derived from a different parameter hierarchy in your SSM Parameter Store.

These are the steps we’ll follow:

A Bit of Contrivance

But first, we’ll set up a little bit of a contrived environment so we can focus just on the WireGuard aspects of this scenario, and not get bogged down in the deployment details of the hosts or other containers. For this example scenario, we’ll say we have two different containers on two different ECS hosts that we want to access through our WireGuard containers. Each of these other containers is running some custom web service at port 80 within the container, and it’s that web service that we want to access through WireGuard.

We’ll add a unique host attribute to each of these two ECS hosts, so we can identify each uniquely, by running the following commands from our local workstation via the AWS CLI:

$ aws ecs put-attributes \
    --cluster prod-apps \
    --attributes name=host,value=apps-123,targetId=arn:aws:ecs:us-west-2:012345678912:container-instance/prod-apps/f25418b76d7a118021a6cbb7a6b8c822
$ aws ecs put-attributes \
    --cluster prod-apps \
    --attributes name=host,value=apps-789,targetId=arn:aws:ecs:us-west-2:012345678912:container-instance/prod-apps/535dec2b8e7a4af31e8aed85bc893fd2

One host we’ll label apps-123, and the other apps-789.

On host apps-123 we’ve deployed one of the containers we want to access through WireGuard, and have determined that its IP address on the ECS host’s bridge network is 172.17.0.3. On host apps-789 we’ve deployed the other container, and determined that its IP address on that ECS host’s bridge network is 172.17.0.9.

Note

There isn’t a good way to look up the IP address of a container on its host’s bridge network through the AWS SDKs — the most practical way of doing this is simply using the Docker CLI on the ECS host itself to find the Docker ID of the container via the docker ps command, and then with that ID (for example 0123457890abc), running the following command to look up its IP address:

$ docker container inspect 012345789abc -f '{{.NetworkSettings.IPAddress}}'
172.17.0.3

You might alternatively have the containers themselves look up the IP address of their own eth0 interface on startup, and report that to some of your other infrastructure.

Now we can start on the WireGuard parts:

Write Config Builder Script

The first step is to write a script that will run in the WireGuard container, pull parameters from the SSM Parameter Store, and assemble them into a WireGuard config file. Here’s a simple Python script that does just that:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Builds WireGuard configuration files from AWS SSM parameters.

Environment variables:
    AWS_REGION: Container's region (eg 'us-west-2').
    WIREGUARD_SSM_PARAMS_PATH: SSM param path to the root of the wg configs.

For example, if `WIREGUARD_SSM_PARAMS_PATH=/prod/infra/wg-p2s`, this script
expects the `/prod/infra/wg-p2s/wg0/Interface/PrivateKey` param will contain
the private key for the `wg0` WireGuard interface, the
`/prod/infra/wg-p2s/wg0/Interface/Address` param will contain the address for
the `wg0` WireGuard interface, etc.
"""

import boto3
import os
import pathlib


def main():
    "Entry point."
    ssm_path = os.environ.get("WIREGUARD_SSM_PARAMS_PATH")
    params = load_params_from_ssm(ssm_path)
    cnf = load_cnf_from_params(params, ssm_path)

    for interface_name, interface_cnf in cnf.items():
        build_interface(interface_name, interface_cnf)


def load_params_from_ssm(path):
    """Loads list of SSM params with the specified root param path.

    Arguments:
        path (str): Root param path (eg '/prod/infra/wg-p2s').

    Returns:
        list: List of SSM param details.
    """
    client = boto3.client("ssm", region_name=os.environ.get("AWS_REGION"))
    paginator = client.get_paginator("get_parameters_by_path")
    pages = paginator.paginate(Path=path, Recursive=True, WithDecryption=True)
    return pages.build_full_result()["Parameters"]


def load_cnf_from_params(params, path):
    """Loads configuration dict from the specified list of SSM params.

    For example, for the path `/prod/infra/wg-p2s`, converts the following list::

        [
            {
                "Name": "/prod/infra/wg-p2s/wg0/Interface/Address",
                "Value": "10.0.0.2"
            },
            {
                "Name": "/prod/infra/wg-p2s/wg0/Interface/ListenPort",
                "Value": "51820"
            },
            {
                "Name": "/prod/infra/wg-p2s/wg0/Interface/PreUp",
                "Value": "iptables -t nat -A POSTROUTING ! -o %i -j MASQUERADE"
            },
            {
                "Name": "/prod/infra/wg-p2s/wg0/Interface/PrivateKey",
                "Value": "ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA="
            },
            {
                "Name": "/prod/infra/wg-p2s/wg0/Peer/EndpointA/AllowedIPs",
                "Value": "10.0.0.1/32"
            },
            {
                "Name": "/prod/infra/wg-p2s/wg0/Peer/EndpointA/PublicKey",
                "Value": "/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU="
            }
        ]

    To the following dict::

        {
            "wg0": {
                "Interface": {
                    "Address": "10.0.0.2",
                    "ListenPort": "51820",
                    "PreUp": "iptables -t nat -A POSTROUTING ! -o %i -j MASQUERADE",
                    "PrivateKey": "ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA="
                },
                "Peer": {
                    "EndpointA": {
                        "AllowedIPs": "10.0.0.1/32",
                        "PublicKey": "/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU="
                    }
                }
            }
        }

    Arguments:
        params: List of SSM param details.
        path (str): Root param path (eg '/prod/infra/wg-p2s').

    Returns:
        dict: Hierarchical dict of SSM param keys and values.
    """
    cnf = {}
    for param in params:
        name = param["Name"]
        value = param["Value"]
        if name and len(name) > len(path) and value:
            subcnf = cnf
            segments = name[len(path) + 1:].split("/")
            for segment in segments[:-1]:
                subcnf = subcnf.setdefault(segment, {})
            subcnf[segments[-1]] = value
    return cnf


def build_interface(name, cnf):
    """Writes a WireGuard config file for the specified interface.

    Arguments:
        name (str): Interface name (eg 'wg0').
        cnf (dict): Hierarchical dict of SSM param keys and values.
    """
    with pathlib.Path("/etc/wireguard", f"{name}.conf").open("w") as f:
        write_interface_section(cnf["Interface"], f)
        for peer, peer_cnf in cnf["Peer"].items():
            write_peer_section(peer, peer_cnf, f)


def write_interface_section(cnf, f):
    """Writes the [Interface] section of a WireGuard config file.

    Arguments:
        cnf (dict): Interface subsection of hierarchical dict of SSM params.
        f (File): File to write to.
    """
    print("[Interface]", file=f)
    for name, value in cnf.items():
        print(f"{name} = {value}", file=f)


def write_peer_section(name, cnf, f):
    """Writes a [Peer] section of a WireGuard config file.

    Arguments:
        name (str): Peer name (for comments).
        cnf (dict): Peer subsection of hierarchical dict of SSM params.
        f (File): File to write to.
    """
    print("", file=f)
    print(f"# {name}", file=f)
    print("[Peer]", file=f)
    for name, value in cnf.items():
        print(f"{name} = {value}", file=f)


if __name__ == "__main__":
    main()

Write OpenRC Init Script

The next step is to write an OpenRC init script that will run the above Python script when the WireGuard container is launched. It needs to run before the container’s wg-quick service, as it builds the WireGuard config file for the WireGuard interface that the wq-quick service itself will start up. Here’s an init script that will do that:

#!/sbin/openrc-run

command=/usr/local/bin/build-wg-conf.py
description="Build WireGuard Configuration"

depend() {
    need localmount
    need net
    before wg-quick
}

Write Custom Dockerfile

The next step is to write a custom Dockerfile that will add the above two scripts to a base WireGuard container image, and run the init script on container startup.

The following Dockerfile will start with the base procustodibus/wireguard image, install Python 3 and Boto 3 support (allowing it to run the Python script and call the AWS SDK), copy your working directory’s fs subdirectory hierarchy into the container at the container’s root, and register the build-wg-conf init script to run on container boot:

FROM procustodibus/wireguard:latest
RUN apk add --no-cache py3-boto3
COPY fs /
RUN rc-update add build-wg-conf default

Build Custom Image

Now organize those three files into their own directory structure like the following, with the Dockerfile at the root of this structure, the Python script in a subdirectory at fs/usr/local/bin/build-wg-conf.py, and the init script in another subdirectory at fs/etc/init.d/build-wg-conf:

$ tree
.
├── Dockerfile
└── fs
    ├── etc
    │   └── init.d
    │       └── build-wg-conf
    └── usr
        └── local
            └── bin
                └── build-wg-conf.py

From the directory containing the above Dockerfile, you can now build the image by running the following Docker command:

$ docker build --tag wg-ssm-params .

We’ll call this image wg-ssm-params.

Push Image to ECR

Next, create a new AWS ECR (Elastic Container Repository) repository for this image:

$ aws ecr create-repository --repository-name wg-ssm-params
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-west-2:012345678912:repository/wg-ssm-params",
        "registryId": "012345678912",
        "repositoryName": "wg-ssm-params",
        "repositoryUri": "012345678912.dkr.ecr.us-west-2.amazonaws.com/wg-ssm-params",
        "createdAt": "2022-05-22T17:12:25-07:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

Then you can tag your locally-built image using the repository URI:

$ docker tag wg-ssm-params 012345678912.dkr.ecr.us-west-2.amazonaws.com/wg-ssm-params

And set up your Docker credentials to allow pushing to this repository:

$ aws ecr get-login-password | docker login \
    --username AWS \
    --password-stdin \
    012345678912.dkr.ecr.us-west-2.amazonaws.com

And finally, push the image:

$ docker push 012345678912.dkr.ecr.us-west-2.amazonaws.com/wg-ssm-params

Define ECS Task

Now the custom WireGuard container image is built, and registered with AWS. Next, we’ll define an ECS task that can be used to run this image. A task definition composed of the following JSON will do the trick:

{
    "family": "wg-ssm-params",
    "containerDefinitions": [
        {
            "name": "wireguard",
            "image": "012345678912.dkr.ecr.us-west-2.amazonaws.com/wg-ssm-params",
            "cpu": 16,
            "memory": 16,
            "portMappings": [
                {
                    "hostPort": 51820,
                    "containerPort": 51820,
                    "protocol": "udp"
                }
            ],
            "linuxParameters": {
                "capabilities": {
                    "add": ["NET_ADMIN"]
                }
            },
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/wireguard",
                    "awslogs-region": "us-west-2",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ]
}

The key difference between this task definition, and the one from the Define the WireGuard Task section of the original WireGuard on AWS ECS article, is that this one uses our custom WireGuard image — and so has no need for any volume bindings. See that article for a discussion of the other task settings.

Save this JSON as a file like wg-ssm-params.json somewhere on your local workstation, and then run the following command from the same directory to register the task:

$ aws ecs register-task-definition --cli-input-json file://wg-ssm-params.json

Set Up SSM Params

Next, we’ll set up the SSM params that our builder script from the Write Config Builder Script section will consume.

For the first host, apps-123, run the following commands to configure a wg0 WireGuard interface for it under the /prod/apps/ecs/123/wireguard SSM param hierarchy:

$ aws ssm put-parameter \
    --type SecureString \
    --name /prod/apps/ecs/123/wireguard/wg0/Interface/PrivateKey \
    --value ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Interface/Address \
    --value 10.0.123.2
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Interface/ListenPort \
    --value 51820
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Interface/PreUp \
    --value 'iptables -t nat -A POSTROUTING -d 172.17.0.3 -j MASQUERADE'
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Peer/MyWorkstation/PublicKey \
    --value /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Peer/MyWorkstation/AllowedIPs \
    --value 10.0.123.1

Since on the apps-123 host, we want the WireGuard container to allow access to another container with an IP address of 172.17.0.3, we add an iptables rule that masquerades connections to 172.17.0.3 via the PreUp configuration setting. For the WireGuard connection itself, we’ll use an IP address of 10.0.123.2 for the WireGuard container (Address setting), and 10.0.123.1 for our WireGuard client (AllowedIPs setting). You can change these WireGuard addresses to whatever you want — just make sure they don’t conflict with any other IP addresses that your WireGuard client needs to access.

We’re using a public / private key of pair of fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= / ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= for the WireGuard container, and a public / private key pair of /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= / AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= for our client (the private key of the container and the public key of the client are used in the container’s config; and the public key of the container and the private key of the client will be used in the client’s config). Don’t you use these particular keys, however — generate WireGuard keys uniquely for each WireGuard peer.

If you want to add more WireGuard clients, just add more Peer parameters with unique keys and IP addresses. For example, to add a client for Alice’s workstation, run these additional commands:

$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Peer/AlicesWorkstation/PublicKey \
    --value jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/123/wireguard/wg0/Peer/AlicesWorkstation/AllowedIPs \
    --value 10.0.123.3

As for the second ECS host, apps-789, run the following commands to set up a wg0 WireGuard interface for it under the /prod/apps/ecs/789/wireguard SSM param hierarchy:

$ aws ssm put-parameter \
    --type SecureString \
    --name /prod/apps/ecs/789/wireguard/wg0/Interface/PrivateKey \
    --value 2G22222222222222222222222222222222222222220=
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/789/wireguard/wg0/Interface/Address \
    --value 10.0.78.2
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/789/wireguard/wg0/Interface/ListenPort \
    --value 51820
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/789/wireguard/wg0/Interface/PreUp \
    --value 'iptables -t nat -A POSTROUTING -d 172.17.0.9 -j MASQUERADE'
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/789/wireguard/wg0/Peer/MyWorkstation/PublicKey \
    --value hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
$ aws ssm put-parameter \
    --type String \
    --name /prod/apps/ecs/789/wireguard/wg0/Peer/MyWorkstation/AllowedIPs \
    --value 10.0.78.1

Since we want this host’s WireGuard container to allow access to another container with an IP address of 172.17.0.9, we include an iptables rule that masquerades connections to 172.17.0.9. And for the WireGuard connection itself, we’ll use an IP address of 10.0.78.2 for the WireGuard container, and 10.0.78.1 for our WireGuard client.

For this WireGuard container, we’ll use a public / private key of pair of 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI= / 2G22222222222222222222222222222222222222220= for the container, and a public / private key pair of hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k= / 0F11111111111111111111111111111111111111110= for the client.

Set Up Policy to Access SSM Params

Now we need to set up the AWS IAM permissions that will allow our task to access these params.

First we need an IAM policy to grant some permissions. All it needs to allow is for our build-wg-conf.py script from above to call the GetParametersByPath SSM API on a subtree of the parameter hierarchy.

For host apps-123, that parameter hierarchy is rooted at /prod/apps/ecs/123/wireguard, so the following policy JSON is what we need. Save it on your local workstation as wg-ssm-params-policy-for-apps-123.json:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ssm:GetParametersByPath",
            "Resource": "arn:aws:ssm:us-west-2:012345678912:parameter/prod/apps/ecs/123/wireguard"
        }
    ]
}

And for host apps-789, save the following policy JSON needed for it as wg-ssm-params-policy-for-apps-789.json:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ssm:GetParametersByPath",
            "Resource": "arn:aws:ssm:us-west-2:012345678912:parameter/prod/apps/ecs/789/wireguard"
        }
    ]
}

Then run the following AWS CLI commands from the same directory to register those two policies; the first as wg-ssm-params-for-apps-123, and the second as wg-ssm-params-for-apps-789:

$ aws iam create-policy \
    --policy-name wg-ssm-params-for-apps-123 \
    --policy-document file://wg-ssm-params-for-apps-123.json
$ aws iam create-policy \
    --policy-name wg-ssm-params-for-apps-789 \
    --policy-document file://wg-ssm-params-for-apps-789.json

Set Up Role for Task

With each of those policies set up, we can set up an IAM task role for each: one to apply to the task launched on host apps-123, and one to apply to the task launched on apps-789.

First save this boilerplate “assume role” policy JSON as sts-assume-role.json on your local workstation:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

This is a generic “assume role” policy that allows a role to be used in general by ECS tasks. Use it to create two task roles, one for apps-123 and one for apps-789, by running the following commands:

$ aws iam create-role \
    --role-name wg-ssm-params-for-apps-123 \
    --assume-role-policy-document file://sts-assume-role.json
$ aws iam create-role \
    --role-name wg-ssm-params-for-apps-789 \
    --assume-role-policy-document file://sts-assume-role.json

Then apply our custom wg-ssm-params-for-apps-123 policy to the role of the same name, and do the same for wg-ssm-params-apps-789, with the following commands:

$ aws iam attach-role-policy \
    --role-name wg-ssm-params-for-apps-123 \
    --policy-arn arn:aws:iam::012345678912:policy/wg-ssm-params-for-apps-123
$ aws iam attach-role-policy \
    --role-name wg-ssm-params-for-apps-789 \
    --policy-arn arn:aws:iam::012345678912:policy/wg-ssm-params-for-apps-789

Run Task with Overrides

Now we’re ready to run the WireGuard task. First we’ll set up some overrides settings for each of the two ECS hosts. This is what allows the same task definition to be run with a different WireGuard configuration on each host.

Save one file as wg-ssm-params-overrides-for-apps-123.json on your local workstation, with the overrides for host apps-123:

{
    "containerOverrides": [
        {
            "name": "wireguard",
            "environment": [
                { "name": "AWS_REGION", "value": "us-west-2" },
                { "name": "WIREGUARD_SSM_PARAMS_PATH", "value": "/prod/apps/ecs/123/wireguard" }
            ]
        }
    ],
    "taskRoleArn": "arn:aws:iam::012345678912:role/wg-ssm-params-apps-123"
}

And save the other file as wg-ssm-params-overrides-for-apps-789.json, with the overrides for host apps-789:

{
    "containerOverrides": [
        {
            "name": "wireguard",
            "environment": [
                { "name": "AWS_REGION", "value": "us-west-2" },
                { "name": "WIREGUARD_SSM_PARAMS_PATH", "value": "/prod/apps/ecs/789/wireguard" }
            ]
        }
    ],
    "taskRoleArn": "arn:aws:iam::012345678912:role/wg-ssm-params-apps-789"
}

Each set of overrides includes its own environment variable value for WIREGUARD_SSM_PARAMS_PATH, pointing to the WireGuard SSM params we set up for a particular host in the Set Up SSM Params section. And each set of overrides also includes the IAM role that grants the task permission to access the SSM params (which we set up in the preceding section).

With these overrides defined, we can finally launch the WireGuard tasks, one into host apps-123, and one into host apps-789:

$ aws ecs run-task \
    --cluster prod-apps \
    --task-definition wg-ssm-params:1 \
    --placement-constraints type=memberOf,expression=attribute:host==apps-123 \
    --overrides file://wg-ssm-params-overrides-for-apps-123.json
$ aws ecs run-task \
    --cluster prod-apps \
    --task-definition wg-ssm-params:1 \
    --placement-constraints type=memberOf,expression=attribute:host==apps-789 \
    --overrides file://wg-ssm-params-overrides-for-apps-789.json

Check Logs

You should see logs like the following when you start up the two tasks:

$ aws logs tail /wireguard
2022-05-23T02:23:05.761000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * /proc is already mounted
2022-05-23T02:23:05.763000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * /run/lock: creating directory
2022-05-23T02:23:05.763000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * /run/lock: correcting owner
2022-05-23T02:23:05.811000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6    OpenRC 0.44.7.88ce4d9bb0 is starting up Linux 4.14.275-207.503.amzn2.x86_64 (x86_64) [DOCKER]
2022-05-23T02:23:05.811000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * Caching service dependencies ... [ ok ]
2022-05-23T02:23:06.171000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * Starting build-wg-conf ... [ ok ]
2022-05-23T02:23:06.186000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] iptables -t nat -A POSTROUTING -d 172.17.0.3 -j MASQUERADE
2022-05-23T02:23:06.380000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] ip link add wg0 type wireguard
2022-05-23T02:23:06.381000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] wg setconf wg0 /dev/fd/63
2022-05-23T02:23:06.382000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] ip -4 address add 10.0.123.2 dev wg0
2022-05-23T02:23:06.384000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] ip link set mtu 1420 up dev wg0
2022-05-23T02:23:06.387000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6 [#] ip -4 route add 10.0.123.1 dev wg0
2022-05-23T02:23:06.388000+00:00 ecs/wireguard/7e5270eecf6548b7a16b83ccaca331b6  * Starting WireGuard interface wg0 ... [ ok ]
2022-05-23T02:23:24.682000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * /proc is already mounted
2022-05-23T02:23:24.684000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * /run/lock: creating directory
2022-05-23T02:23:24.684000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * /run/lock: correcting owner
2022-05-23T02:23:25.202000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc    OpenRC 0.44.7.88ce4d9bb0 is starting up Linux 4.14.275-207.503.amzn2.x86_64 (x86_64) [DOCKER]
2022-05-23T02:23:25.202000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * Caching service dependencies ... [ ok ]
2022-05-23T02:23:25.411000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * Starting build-wg-conf ... [ ok ]
2022-05-23T02:23:25.556000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] iptables -t nat -A POSTROUTING -d 172.17.0.9 -j MASQUERADE
2022-05-23T02:23:25.770000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] ip link add wg0 type wireguard
2022-05-23T02:23:25.772000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] wg setconf wg0 /dev/fd/63
2022-05-23T02:23:25.773000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] ip -4 address add 10.0.78.2 dev wg0
2022-05-23T02:23:25.773000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] ip link set mtu 1420 up dev wg0
2022-05-23T02:23:25.775000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc [#] ip -4 route add 10.0.78.1 dev wg0
2022-05-23T02:23:25.776000+00:00 ecs/wireguard/361e4130b74f862beb858f162a4f87cc  * Starting WireGuard interface wg0 ... [ ok ]

If you see an AccessDeniedException like the following, however, it means that the WIREGUARD_SSM_PARAMS_PATH environment variable for the task has specified a SSM parameter path which the task does not have permission to access:

botocore.exceptions.ClientError: An error occurred (AccessDeniedException) when calling the GetParametersByPath operation: User: arn:aws:sts::012345789012:assumed-role/ecsInstanceRole/i-0123456789abcdef0 is not authorized to perform: ssm:GetParametersByPath on resource: arn:aws:ssm:us-west-2:012345789012:parameter/prod/apps/ecs/123/wireguard because no identity-based policy allows the ssm:GetParametersByPath action

Make sure you correctly connected the SSM params to a policy, the policy to a role, and the role to the task, as shown in the above Set Up Policy to Access SSM Params, Set Up Role for Task, and Run Task with Overrides sections.

See the Troubleshooting section of the original WireGuard on AWS ECS article for other errors.

Connect Through WireGuard

Once the first task is up and running, you can connect to its WireGuard container from your workstation with the following WireGuard config file:

# /etc/wireguard/wg-apps-123.conf

# local settings for my workstation
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.123.1

# remote settings for apps-123 WireGuard container
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.12:51820
AllowedIPs = 172.17.0.3

See the WireGuard Point to Site Configuration article for a detailed explanation of each setting used in this config; but note that:

  • The PrivateKey used in this config file should correspond to the PublicKey setting for some peer listed in the container’s config file (when you generate a WireGuard key for a client in this scenario, you put the private key in the client’s config, and the public key in the container’s config).

  • The Address used in this config file should be the same as the AllowedIPs setting for the same peer in the container’s config file (in this example, we used 10.0.123.1 for my workstation).

  • The PublicKey used in this config file should correspond to the PrivateKey setting for the container’s config file (when you generate the WireGuard key for the container, you put the private key in the container’s config, and copy the public key into all the client configs).

  • The Endpoint used in this config file should be the public IP address (or public DNS entry) of the ECS host running the WireGuard container, with the port matching the hostPort from the task’s definition.

  • The AllowedIPs used in this config file should be the IP address of the other container running on the ECS host that you want to access through this WireGuard connection (172.17.0.3 in this case).

Assuming that you have WireGuard installed on your local workstation, you can save the above config file as /etc/wireguard/wg-apps-123.conf, and run the following command on your workstation to start up the WireGuard connection to the remote container:

$ sudo wg-quick up wg-apps-123

With the WireGuard connection up, you can access the web service in other container using its local IP address on the ECS host’s bridge network:

$ curl 172.17.0.3

And once the second task is up and running, you can connect to its WireGuard container from your workstation with the following WireGuard config file:

# /etc/wireguard/wg-apps-789.conf

# local settings for my workstation
[Interface]
PrivateKey = 0F11111111111111111111111111111111111111110=
Address = 10.0.78.1

# remote settings for apps-789 WireGuard container
[Peer]
PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
Endpoint = 198.51.100.77:51820
AllowedIPs = 172.17.0.9

Save the above config file as /etc/wireguard/wg-apps-789.conf, and start up the WireGuard connection.:

$ sudo wg-quick up wg-apps-789

With the WireGuard connection up, you can access the web service in the other container on host-789 using its local IP address on the ECS host’s bridge network:

$ curl 172.17.0.9