AzureDevOps · docker · ServicePrincipal

Azure DevOps Container agents without PAT

Azure DevOps now supports Service principals and Managed identities for authentication. This is currently (at the time of writing) in Public Preview. This is quite an interesting feature and addresses many concerns related to personal access tokens (PATs). I have a self-hosted container agent runs on Docker which needs PAT to spin up. With the new development I wanted see if I can get rid of PAT and run the agent with service principal-based authentication.  Here’s how I made it to work.

Building Container image for Agent

Here’s the official document that describes the process of creating self-hosted agents for Azure DevOps using Docker. We will modify the start.sh file slightly to use service principal instead of personal access tokens (PATs). Here’s our modified start.sh file.

#!/bin/bash
set -e
resource="499b84ac-1321-427f-aa17-267ca6975798/.default"
if [ -z "$AZP_URL" ]; then
  echo 1>&2 "error: missing AZP_URL environment variable"
  exit 1
fi

if [ -n "$AZP_CLIENTID" ]; then           
           AZP_TOKEN=$(curl -X POST -d "grant_type=client_credentials&client_id=$AZP_CLIENTID&client_secret=$AZP_CLIENTSECRET&resource=$resource" https://login.microsoftonline.com/$AZP_TENANTID/oauth2/token | jq -r '.access_token')
fi


if [ -z "$AZP_TOKEN_FILE" ]; then
  if [ -z "$AZP_TOKEN" ]; then
    echo 1>&2 "error: missing AZP_TOKEN environment variable"
    exit 1
  fi

  AZP_TOKEN_FILE=/azp/.token
  echo -n $AZP_TOKEN > "$AZP_TOKEN_FILE"
fi

unset AZP_TOKEN

if [ -n "$AZP_WORK" ]; then
  mkdir -p "$AZP_WORK"
fi

export AGENT_ALLOW_RUNASROOT="1"

cleanup() {
  if [ -e config.sh ]; then
    print_header "Cleanup. Removing Azure Pipelines agent..."

    # If the agent has some running jobs, the configuration removal process will fail.
    # So, give it some time to finish the job.
    while true; do
      ./config.sh remove --unattended --auth PAT --token $(cat "$AZP_TOKEN_FILE") && break

      echo "Retrying in 30 seconds..."
      sleep 30
    done
  fi
}

print_header() {
  lightcyan='\033[1;36m'
  nocolor='\033[0m'
  echo -e "${lightcyan}$1${nocolor}"
}

# Let the agent ignore the token env variables
export VSO_AGENT_IGNORE=AZP_TOKEN,AZP_TOKEN_FILE

print_header "1. Determining matching Azure Pipelines agent..."

AZP_AGENT_PACKAGES=$(curl -LsS \
    -u user:$(cat "$AZP_TOKEN_FILE") \
    -H 'Accept:application/json;' \
    "$AZP_URL/_apis/distributedtask/packages/agent?platform=$TARGETARCH&top=1")

AZP_AGENT_PACKAGE_LATEST_URL=$(echo "$AZP_AGENT_PACKAGES" | jq -r '.value[0].downloadUrl')

if [ -z "$AZP_AGENT_PACKAGE_LATEST_URL" -o "$AZP_AGENT_PACKAGE_LATEST_URL" == "null" ]; then
  echo 1>&2 "error: could not determine a matching Azure Pipelines agent"
  echo 1>&2 "check that account '$AZP_URL' is correct and the token is valid for that account"
  exit 1
fi

print_header "2. Downloading and extracting Azure Pipelines agent..."

curl -LsS $AZP_AGENT_PACKAGE_LATEST_URL | tar -xz & wait $!

source ./env.sh

print_header "3. Configuring Azure Pipelines agent..."

./config.sh --unattended \
  --agent "${AZP_AGENT_NAME:-$(hostname)}" \
  --url "$AZP_URL" \
  --auth PAT \
  --token $(cat "$AZP_TOKEN_FILE") \
  --pool "${AZP_POOL:-Default}" \
  --work "${AZP_WORK:-_work}" \
  --replace \
  --acceptTeeEula & wait $!

print_header "4. Running Azure Pipelines agent..."

trap 'cleanup; exit 0' EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

chmod +x ./run-docker.sh

# To be aware of TERM and INT signals call run.sh
# Running it with the --once flag at the end will shut down the agent after the build is executed
./run-docker.sh "$@" & wait $!

The important part that differs from official documentation is, we are checking collecting an AAD access token and feed that back to the downstream as PAT – apparently it works flawlessly.

Next, we would need the Dockerfile:

FROM ubuntu:20.04
 
LABEL Author="Moim Hossain"
LABEL Email="moimhossain@gmail.com"
LABEL GitHub="https://github.com/moimhossain"
LABEL BaseImage="ubuntu:20.04"
 
RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && useradd -m agentuser
 
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
    apt-transport-https \
    apt-utils \
    ca-certificates \
    curl \
    git \
    iputils-ping \
    jq \
    lsb-release \
    software-properties-common
 
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
ENV TARGETARCH=linux-x64
 
WORKDIR /azp
RUN chown -R agentuser:agentuser /azp
RUN chmod 755 /azp
 
COPY ./start.sh .
RUN chmod +x start.sh
# All subsequent commands run under this user
USER agentuser
RUN ls /azp 
ENTRYPOINT [ "./start.sh", "--once" ]

Build Image

We can now build this image from Docker CLI:

docker build -t moimhossain/agent:v1 .

Run the container

Let’s run it now:

docker run --rm  \
        -e AZP_URL=https://dev.azure.com/ORG \
        -e AZP_AGENT_NAME=selfhosted-pool \
        -e AZP_CLIENTID=XXX \
        -e AZP_CLIENTSECRET=YYY \
        -e AZP_TENANTID=ZZZ \    
        moimhossain/agent:v1

Conclusion

This is a preview feature, and I took played the hack to make it work. Which was fun but I don’t recommend using it in production or any serious environment.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s