Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions .github/workflows/build-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
workflow_run:
workflows: ["build-test"]
types:
- completed

env:
REGISTRY: ghcr.io
Expand All @@ -19,6 +23,10 @@ jobs:
packages: write
id-token: write


# only run on success of "build-test" workflow
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Checkout repository
uses: actions/checkout@v2
Expand All @@ -27,19 +35,21 @@ jobs:
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: startsWith(github.ref, 'refs/tags/v')
uses: sigstore/cosign-installer@v3
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v1.4.0'
cosign-release: 'v2.2.4'

# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand All @@ -49,7 +59,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -63,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
build-args: |
Expand All @@ -80,7 +90,9 @@ jobs:
- name: Sign the published Docker image
if: startsWith(github.ref, 'refs/tags/v')
env:
COSIGN_EXPERIMENTAL: "true"
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: cosign sign ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
63 changes: 63 additions & 0 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# NOTE: Add Action Secrets for the following variables:
#
# - AWS_ACCESS_KEY_ID
# - AWS_SECRET_ACCESS_KEY
# - AWS_REGION
#

name: build-test

on:
pull_request:
branches: [ main ]

env:
HOME: ${{ github.workspace }}
AWS_PROFILE: "default"

jobs:
workflow:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}

# https://github.com/aws-actions/configure-aws-credentials/issues/112
# - name: Configure AWS credentials
# uses: aws-actions/configure-aws-credentials@v4
# with:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: ${{ secrets.AWS_REGION }}

- name: Configure AWS credentials (HACK)
run: |
aws configure set profile.${AWS_PROFILE}.aws_access_key_id "${{ secrets.AWS_ACCESS_KEY_ID }}"
aws configure set profile.${AWS_PROFILE}.aws_secret_access_key "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
aws configure set profile.${AWS_PROFILE}.region "${{ secrets.AWS_REGION }}"

- name: Build
run: task build

- name: Test
run: task test -- true

# example can't run on GitHub Actions due to compose network configuration
# since access to the internet isn't possible from within the container network
# - name: Test Example
# run: task example

- name: Cleanup
run: task clean
5 changes: 1 addition & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ COPY README.md LICENSE /opt/metadock/
ENV PORT=80

HEALTHCHECK --interval=5s \
--timeout=10s \
--start-period=1s \
--retries=5 \
CMD curl -sSfL http://127.0.0.1:${PORT}/health/ > /dev/null

ENTRYPOINT ["/sbin/tini", "--", "/opt/metadock/bin/metadock"]
Expand All @@ -52,7 +49,7 @@ RUN apt-get update -qq \

RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o awscli-exe-linux.zip \
&& tmpdir=$(mktemp -d) \
&& unzip awscli-exe-linux.zip -d "${tmpdir}" \
&& unzip -q awscli-exe-linux.zip -d "${tmpdir}" \
&& "${tmpdir}/aws/install" \
&& rm -rf awscli-exe-linux.zip "${tmpdir}"

Expand Down
132 changes: 84 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

## Overview

`MetaDock` provides a lightweight Go implementation of the [AWS Instance Metadata Service (IMDSv2)][imds].
`MetaDock` provides a lightweight Go implementation of a subset of the [AWS Instance Metadata Service (IMDSv2)][imds]
for the purpose of providing session credentials only.

It is designed to run inside a Docker container and can be included in a `docker-compose` setup to
provide AWS credentials to other services, effectively emulating an EC2 environment locally.

Instead of retrieving credentials from AWS, the emulator loads them from the host, via the mounted
`${HOME}/.aws` directory, using the `aws configure export-credentials` command. It then exposing
them through the same API paths that would normally be available inside an EC2 instance:
`${HOME}/.aws` directory, and then exposes them through the same API paths that would normally be
available inside an EC2 instance:

* `/latest/api/token`
* `/latest/meta-data/iam/security-credentials`
Expand All @@ -21,88 +22,120 @@ them through the same API paths that would normally be available inside an EC2 i
`MetaDock` responds with the same metadata format as a real EC2 instance, enabling AWS SDKs and CLI
commands inside containers to authenticate transparently.

It relies on the developer obtaining AWS credentials on the host machine *before* running the
`metadock` service, and attaching the services which require the service to the emulators docker
network.
It relies on the developer obtaining AWS credentials on the host machine *before* running the `metadock`
service. If long-lived (static) credentials are found, the service will generate session credentials,
with a default expiry of 12 hours.

## Usage

### Prerequisites

* Host machine with:
* AWS CLI v2 installed and configured (`aws configure`, `aws sso login`, or equivalent)
* AWS configuration profile with valid credentials.
* Docker and `docker-compose`
* [Task][task] for running development tasks

- Linux
- [Task][task]
- Docker and `docker-compose`
- AWS CLI v2 installed and configured (`aws configure`, `aws sso login`, or equivalent)
### Quickstart

* An existing AWS profile with valid credentials.
#### Ensure you are logged into AWS and have valid credentials

### Quickstart
```bash
aws configure
```

1. Ensure you are logged into AWS:
Or

```bash
aws configure
```
```bash
aws sso login [--profile profile-name]
````

Or
#### Include the `metadock` service in your Docker Compose configuration

```bash
aws sso login [--profile profile-name]
````
The `MetaDock` service can be used in one of the following ways.

2. Include `compose.metadock.yml` in your `docker-compose.yml`.
1. Using the provided [`compose.metadock.yml`](compose.metadock.yml) file.

- Use `include` directive to include the supplied [`compose.metadock.yml`](compose.metadock.yml) file.
- Add the `metadock` network to the services which need AWS credentials.
- Use an [`include` directive][include] to include the [`compose.metadock.yml`](compose.metadock.yml) file.
- Add the `metadock` network to the services which need IMDS.

See [`compose.example.yml`](compose.example.yml) for an example configuration.

3. Your services can now query the emulator.
2. Adding the `metadock` service and configure the `AWS_EC2_METADATA_SERVICE_ENDPOINT` environment variable.

Edit your compose file, add the `metadock` service and configure services which need IMDS.

Optionally, from within the service (using `docker exec ...`):
```yaml
services:

metadock:
image: "ghcr.io/virtualstaticvoid/metadock:latest"
command: "${AWS_PROFLE:-default}"
volumes:
- "${HOME}/.aws:/root/.aws:ro"

* Use `curl` to check if the `MetaDock` service is accessible
your_service:
image: "..."
env:
AWS_EC2_METADATA_SERVICE_ENDPOINT: http://metadock/

```

```bash
curl http://metadock/
#### Your services can now query the emulator

# => MetaDock
```
Optionally, from within the respective services (using the `docker exec` command).

* Or, check by running `aws` CLI commands
* If `curl` is installed in the container, check if the `MetaDock` service is issuing credentials.

```bash
TOKEN=$(curl -X PUT "http://metadock/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://metadock/latest/meta-data/iam/security-credentials/metadock
# => {"AccessKeyId":"...", ...}
```

See [Use the Instance Metadata Service to access instance metadata][using-imds] documentation for details.

```bash
aws s3 ls
```
* If the [`aws` CLI tool][aws-cli] is installed in the container, check by running the `sts get-caller-identity` CLI command.

```bash
aws sts get-caller-identity --no-cli-pager
# => {"UserId": "...", ...}
```

## Building and Testing

This project uses [Task][task] to manage common development workflows.

* Build the service
### Build the service

```bash
task build
```
```bash
task build
```

* Run tests
#### Run tests

```bash
task test
```
```bash
task test
```

* Cleanup artifacts
#### Cleanup artifacts

```bash
task clean
```
```bash
task clean
```

Alternatively, you can build manually:
Alternatively, you can run it directly on the host.

```bash
go build -o metadock .
PORT=8080 ./metadock <profile-name>
```

And connect, via `localhost` with the configured `PORT`.

```bash
TOKEN=$(curl -X PUT "http://localhost:8080/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://localhost:8080/latest/meta-data/iam/security-credentials/metadock
# => {"AccessKeyId":"...", ...}
```

## License
Expand All @@ -111,5 +144,8 @@ MIT License. Copyright (c) 2025 Chris Stefano. See [LICENSE](LICENSE) for detail

<!-- links -->

[aws-cli]: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
[imds]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
[include]: https://docs.docker.com/reference/compose-file/include/
[task]: https://taskfile.dev/
[using-imds]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
11 changes: 6 additions & 5 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ tasks:
desc: "Run tests."
vars:
AWS_PROFILE: '{{.AWS_PROFILE | default "default"}}'
HIDE_OUTPUT: '{{.CLI_ARGS | default ""}}'
env:
COMPOSE_FILE: "compose.metadock.yml:compose.test.yml"
COMPOSE_FILE: "compose.test.yml"
cmds:
- echo "Running tests using '{{.AWS_PROFILE}}' AWS profile..."
- docker compose up --force-recreate --detach
- docker compose up --force-recreate --detach --wait || { docker compose logs; exit 1; }
- docker compose logs metadock
- docker compose exec -it test /test.sh
- docker compose exec -it test /test.sh {{if .HIDE_OUTPUT}}> /dev/null{{end}}

example:
desc: "Run example."
Expand All @@ -39,9 +40,9 @@ tasks:
COMPOSE_FILE: "compose.example.yml"
cmds:
- echo "Running example using '{{.AWS_PROFILE}}' AWS profile..."
- docker compose up --force-recreate --detach
- docker compose up --force-recreate --detach --wait || { docker compose logs; exit 1; }
- docker compose logs metadock
- docker compose exec -it example aws s3 ls
- docker compose exec -it example aws sts get-caller-identity --no-cli-pager

clean:
desc: "Stop services and clean up."
Expand Down
5 changes: 5 additions & 0 deletions compose.example.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#
# MetaDock
# https://github.com/virtualstaticvoid/metadock
# MIT License. Copyright (c) 2025 Chris Stefano
#
---
include:
- compose.metadock.yml
Expand Down
Loading