Skip to content

Self-Hosted Runners

Run Controlinfra scans in your own infrastructure for enhanced security and compliance.

Overview

Self-hosted runners allow you to:

  • Keep credentials local: Cloud credentials never leave your infrastructure
  • Meet compliance requirements: Run scans within your security perimeter
  • Use cloud-native auth: IAM roles (AWS), Managed Identity (Azure), or Workload Identity (GCP)
  • Control the environment: Your infrastructure, your rules

Supported Cloud Platforms

PlatformVM TypeAuthenticationGuide
AWSEC2Instance Profile, Assume RoleAWS Setup
AzureAzure VMManaged IdentityAzure Setup
GCPCompute EngineService Account, Workload IdentityGCP Setup

How It Works

┌─────────────────────────────────────────────────────────────┐
│                     Your Infrastructure                      │
│  ┌─────────────────┐    ┌───────────────────────────────┐  │
│  │  Self-Hosted    │    │       Your Cloud Resources    │  │
│  │    Runner       │───▶│  (VMs, Databases, Storage)    │  │
│  │                 │    └───────────────────────────────┘  │
│  │  • Terraform    │                                       │
│  │  • Cloud CLI    │    ┌───────────────────────────────┐  │
│  │  • Git          │───▶│    Terraform State            │  │
│  └────────┬────────┘    └───────────────────────────────┘  │
│           │                                                 │
└───────────┼─────────────────────────────────────────────────┘
            │ Results only

┌─────────────────────────────────────────────────────────────┐
│                    Controlinfra Cloud                        │
│  • Receives scan results                                    │
│  • Displays in dashboard                                    │
│  • AI analysis (using your API key)                         │
└─────────────────────────────────────────────────────────────┘

AWS EC2 Runner

Prerequisites

  • An EC2 instance in your AWS account
  • Network access to your AWS resources
  • IAM role with read permissions
  • Outbound internet access (HTTPS to Controlinfra)

Setup Guide

Step 1: Create IAM Role

Create an IAM role for the runner with the necessary permissions.

The simplest approach is to attach AWS managed policies:

bash
# Create the IAM role
aws iam create-role \
  --role-name ControlinfraRunnerRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "ec2.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }'

# Attach ReadOnlyAccess for drift detection across all AWS services
aws iam attach-role-policy \
  --role-name ControlinfraRunnerRole \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

# Create and attach Terraform state access policy
aws iam put-role-policy \
  --role-name ControlinfraRunnerRole \
  --policy-name TerraformStateAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "TerraformStateS3",
        "Effect": "Allow",
        "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:GetBucketLocation"],
        "Resource": [
          "arn:aws:s3:::YOUR-TERRAFORM-STATE-BUCKET",
          "arn:aws:s3:::YOUR-TERRAFORM-STATE-BUCKET/*"
        ]
      },
      {
        "Sid": "TerraformDynamoDBLock",
        "Effect": "Allow",
        "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],
        "Resource": "arn:aws:dynamodb:*:*:table/terraform-*"
      }
    ]
  }'

# Create instance profile and attach role
aws iam create-instance-profile --instance-profile-name ControlinfraRunnerProfile
aws iam add-role-to-instance-profile \
  --instance-profile-name ControlinfraRunnerProfile \
  --role-name ControlinfraRunnerRole

Replace Placeholder Values

Replace YOUR-TERRAFORM-STATE-BUCKET with your actual S3 bucket name where Terraform state is stored.

Required Permissions Summary

PermissionPurpose
ReadOnlyAccessQuery AWS resources for drift detection (EC2, RDS, IAM, Lambda, etc.)
S3 State AccessRead Terraform state file from S3 backend
DynamoDB LockTerraform state locking (if using DynamoDB)

Option B: Custom Least-Privilege Policy

For more restrictive access, create a custom policy:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TerraformReadAccess",
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "s3:GetObject",
        "s3:GetBucket*",
        "s3:ListBucket",
        "rds:Describe*",
        "iam:Get*",
        "iam:List*",
        "lambda:Get*",
        "lambda:List*",
        "dynamodb:Describe*",
        "elasticloadbalancing:Describe*",
        "autoscaling:Describe*",
        "cloudwatch:Describe*",
        "sns:Get*",
        "sqs:Get*",
        "route53:Get*",
        "route53:List*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "TerraformStateAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::your-terraform-state-bucket",
        "arn:aws:s3:::your-terraform-state-bucket/*"
      ]
    },
    {
      "Sid": "DynamoDBLocking",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:*:*:table/terraform-*"
    }
  ]
}

When to Use Custom Policy

Use a custom policy when you want to limit access to specific AWS services that your Terraform manages. Add or remove service-specific Describe*/Get*/List* actions based on your infrastructure.

Step 2: Launch EC2 Instance

Launch an EC2 instance with:

SettingRecommendation
AMIAmazon Linux 2023 or Ubuntu 22.04
Instance Typet3.small or larger
IAM RoleThe role created in Step 1
Security GroupOutbound HTTPS (443) only
Storage20GB+ (for Terraform providers)

Step 3: Install Dependencies

SSH into your instance and run:

bash
#!/bin/bash

# Update system
sudo yum update -y  # Amazon Linux
# sudo apt update && sudo apt upgrade -y  # Ubuntu

# Install Git
sudo yum install -y git
# sudo apt install -y git

# Install Terraform
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum install -y terraform

# Verify installations
git --version
terraform --version
aws --version

Step 4: Register Runner in Controlinfra

  1. Go to SettingsRunners
  2. Click "Add Runner"
  3. Copy the registration token

Step 5: Install Runner Agent

On your EC2 instance, first install the Controlinfra CLI:

bash
# Install Controlinfra CLI
curl -fsSL https://controlinfra.com/cli/install.sh | bash

# Verify installation
controlinfra --version

Then authenticate and set up the runner:

bash
# Authenticate with your CLI token (from Settings > CLI Tokens)
controlinfra login --token YOUR_CLI_TOKEN

# Get the runner setup script (replace RUNNER_ID with your runner ID from Step 4)
controlinfra runners setup RUNNER_ID

The runners setup command will output an installation script. Run it to install and start the runner agent:

bash
# Example output - run the command shown by 'runners setup'
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=RUNNER_TOKEN" | sudo bash

Alternative: Direct Installation

If you already have the runner token from Step 4, you can install directly:

bash
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=YOUR_RUNNER_TOKEN" | sudo bash

Step 6: Verify Connection

In Controlinfra:

  1. Go to SettingsRunners
  2. Your runner should show as "Online"

Runner Configuration

Configuration File

The runner configuration is stored at ~/.controlinfra/runner.yml:

yaml
token: "your-runner-token"
name: "production-runner"
labels:
  - production
  - aws
  - us-east-1
workDir: "/tmp/controlinfra"
logLevel: "info"

Environment Variables

Configure the runner using environment variables:

bash
export CONTROLINFRA_TOKEN="your-token"
export CONTROLINFRA_RUNNER_NAME="production-runner"
export CONTROLINFRA_WORK_DIR="/tmp/controlinfra"
export CONTROLINFRA_LOG_LEVEL="debug"

Labels

Use labels to target specific runners:

yaml
labels:
  - production
  - aws
  - us-east-1

Then in repository settings, specify which runner to use:

Runner: production, aws

Running as a Service

systemd Service

Create /etc/systemd/system/controlinfra-runner.service:

ini
[Unit]
Description=Controlinfra Runner
After=network.target

[Service]
Type=simple
User=controlinfra
ExecStart=/usr/local/bin/controlinfra-runner start
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start:

bash
sudo systemctl enable controlinfra-runner
sudo systemctl start controlinfra-runner

Docker

Run the runner in Docker:

bash
docker run -d \
  --name controlinfra-runner \
  --restart always \
  -e CONTROLINFRA_TOKEN=your-token \
  -v /var/run/docker.sock:/var/run/docker.sock \
  controlinfra/runner:latest

Security Best Practices

1. Network Isolation

  • Place runner in a private subnet
  • Use NAT Gateway for outbound access
  • Restrict inbound access completely

2. IAM Least Privilege

  • Only grant read permissions needed
  • Use resource-level permissions where possible
  • Regularly audit IAM policies

3. Secrets Management

bash
# Don't store secrets in environment variables
# Use AWS Secrets Manager or Parameter Store
aws secretsmanager get-secret-value --secret-id controlinfra/token

4. Logging and Monitoring

bash
# Enable CloudWatch logging
sudo yum install -y amazon-cloudwatch-agent

5. Regular Updates

bash
# Update runner agent regularly
controlinfra-runner update

Multiple Runners

For high availability or multi-environment setups:

Environment-Specific Runners

production-runner (labels: production, us-east-1)
   └── Scans production repositories

staging-runner (labels: staging, us-east-1)
   └── Scans staging repositories

eu-runner (labels: production, eu-west-1)
   └── Scans EU region resources

Load Balancing

Multiple runners with the same labels will automatically load balance:

runner-1 (labels: production)
runner-2 (labels: production)
runner-3 (labels: production)

Assigning Runners to Repositories

In Repository Settings

  1. Navigate to your repository
  2. Go to SettingsRunner
  3. Select runner type:
    • Cloud Runner: Use Controlinfra's hosted runner
    • Self-Hosted: Use your own runner
  4. If self-hosted, select the runner or specify labels

Using Labels

yaml
Repository: my-terraform-repo
Runner Type: Self-Hosted
Runner Labels: production, us-east-1

Troubleshooting

Runner Shows "Offline"

  1. Check the runner service is running:

    bash
    systemctl status controlinfra-runner
  2. Check network connectivity:

    bash
    curl -v https://api.controlinfra.com/health
  3. Verify the token:

    bash
    controlinfra-runner verify

Scans Failing on Runner

  1. Check runner logs:

    bash
    journalctl -u controlinfra-runner -f
  2. Verify Terraform is installed:

    bash
    terraform version
  3. Check IAM permissions:

    bash
    aws sts get-caller-identity

"Permission Denied" Errors

  • Verify IAM role is attached to the instance
  • Check IAM policy has required permissions
  • Ensure the work directory is writable

Terraform Init Failures

Common causes and solutions:

  1. S3 State Access Denied:

    Error: Failed to get state: AccessDenied: Access Denied
    • Ensure the IAM role has s3:ListBucket and s3:GetObject on your state bucket
    • Check both bucket-level (s3:ListBucket) and object-level (s3:GetObject) permissions
    • Verify the bucket ARN matches exactly: arn:aws:s3:::your-bucket-name
  2. DynamoDB Lock Access Denied:

    Error: Error acquiring the state lock
    • Add dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem permissions
    • Check the table name pattern matches your lock table
  3. Network/Registry Issues:

    • Check network access to Terraform registries
    • Verify outbound HTTPS (443) is allowed
    • Check disk space for provider downloads
  4. Missing ReadOnlyAccess:

    Error: error reading EC2 instance: UnauthorizedOperation
    • Attach the AWS managed ReadOnlyAccess policy
    • Or add specific Describe*/Get*/List* actions for your resources

Azure VM Runner

Deploy a self-hosted runner on an Azure Virtual Machine using Managed Identity for secure, credential-free authentication.

Prerequisites

  • An Azure subscription
  • An Azure Virtual Machine (Ubuntu 22.04 recommended)
  • System-assigned Managed Identity enabled
  • Outbound internet access (HTTPS to Controlinfra)

Step 1: Create Azure VM with Managed Identity

bash
# Create resource group
az group create \
  --name controlinfra-runner-rg \
  --location eastus

# Create VM with system-assigned managed identity
az vm create \
  --resource-group controlinfra-runner-rg \
  --name controlinfra-runner-vm \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --admin-username adminuser \
  --generate-ssh-keys \
  --assign-identity

# Note the public IP for SSH access

Or enable managed identity on an existing VM:

bash
az vm identity assign \
  --resource-group YOUR_RESOURCE_GROUP \
  --name YOUR_VM_NAME

Step 2: Assign Required Roles

The managed identity needs permissions to read your Azure resources and access Terraform state.

bash
# Get the principal ID of the managed identity
PRINCIPAL_ID=$(az vm identity show \
  --resource-group controlinfra-runner-rg \
  --name controlinfra-runner-vm \
  --query principalId -o tsv)

# Assign Reader role on the resource group containing your infrastructure
az role assignment create \
  --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Reader" \
  --scope /subscriptions/YOUR_SUB_ID/resourceGroups/YOUR_INFRA_RG

# Assign Storage Blob Data Contributor on your Terraform state storage account
az role assignment create \
  --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope /subscriptions/YOUR_SUB_ID/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE

# For full drift detection, assign Contributor on the infrastructure resource group
az role assignment create \
  --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope /subscriptions/YOUR_SUB_ID/resourceGroups/YOUR_INFRA_RG

Required Permissions - All Three Are Needed

RoleScopePurpose
ReaderResource GroupBasic read access to Azure resources
Storage Blob Data ContributorStorage AccountAccess Terraform state file
ContributorResource GroupRequired for listKeys/action and full resource state

Important: Terraform needs Microsoft.Storage/storageAccounts/listKeys/action to read storage account state. Without Contributor role, you'll get "AuthorizationFailed" errors during scans.

Step 3: Install Dependencies

SSH into your Azure VM and install required tools:

bash
#!/bin/bash

# Update system
sudo apt update && sudo apt upgrade -y

# Install Git
sudo apt install -y git curl jq

# Install Terraform
sudo apt install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \
  gpg --dearmor | \
  sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y terraform

# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Verify installations
git --version
terraform --version
az --version

Step 4: Register Runner in Controlinfra

  1. Go to SettingsRunners
  2. Click "Add Runner"
  3. Enter a name and add the azure label
  4. Copy the registration token and runner ID

Step 5: Install Runner Agent

On your Azure VM, first install the Controlinfra CLI:

bash
# Install Controlinfra CLI
curl -fsSL https://controlinfra.com/cli/install.sh | bash

# Verify installation
controlinfra --version

Then authenticate and set up the runner:

bash
# Authenticate with your CLI token (from Settings > CLI Tokens)
controlinfra login --token YOUR_CLI_TOKEN

# Get the runner setup script (replace RUNNER_ID with your runner ID from Step 4)
controlinfra runners setup RUNNER_ID

The runners setup command will output an installation script. Run it to install the runner agent:

bash
# Example output - run the command shown by 'runners setup'
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=RUNNER_TOKEN" | sudo bash

Alternative: Direct Installation

If you already have the runner token from Step 4, you can install directly:

bash
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=YOUR_RUNNER_TOKEN" | sudo bash

Step 6: Configure Managed Identity Environment

Create the systemd service file at /etc/systemd/system/controlinfra-runner.service:

ini
[Unit]
Description=Controlinfra Self-Hosted Runner
After=network.target

[Service]
Type=simple
User=controlinfra
WorkingDirectory=/opt/controlinfra-runner

# Azure Managed Identity configuration for Terraform
Environment="ARM_USE_MSI=true"
Environment="ARM_SUBSCRIPTION_ID=YOUR_SUBSCRIPTION_ID"
Environment="ARM_TENANT_ID=YOUR_TENANT_ID"

ExecStart=/opt/controlinfra-runner/bin/controlinfra-agent
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Important

The ARM_USE_MSI=true environment variable tells Terraform to use Managed Identity instead of Azure CLI authentication. Without this, you'll see "Azure CLI auth only supported as User" errors.

Enable and start:

bash
sudo systemctl daemon-reload
sudo systemctl enable controlinfra-runner
sudo systemctl start controlinfra-runner

Step 7: Verify Connection

  1. Check runner service status:

    bash
    sudo systemctl status controlinfra-runner
  2. View logs:

    bash
    sudo journalctl -u controlinfra-runner -f
  3. In Controlinfra, go to SettingsRunners - your runner should show as "Online"

Troubleshooting Azure Runner

"Azure CLI auth only supported as User" Error

Error building ARM Config: Authenticating using the Azure CLI is only supported as a User (not a Service Principal)

Solution: Add ARM environment variables to the systemd service:

ini
Environment="ARM_USE_MSI=true"
Environment="ARM_SUBSCRIPTION_ID=your-sub-id"
Environment="ARM_TENANT_ID=your-tenant-id"

Then restart: sudo systemctl daemon-reload && sudo systemctl restart controlinfra-runner

"AuthorizationPermissionMismatch" Error

This request is not authorized to perform this operation using this permission

Solution: Assign Storage Blob Data Contributor role on the Terraform state storage account.

"listKeys/action" Permission Error

does not have authorization to perform action 'Microsoft.Storage/storageAccounts/listKeys/action'

Solution: Assign Contributor role on the resource group containing your infrastructure.

Runner Shows "Offline"

  1. Check the service is running:

    bash
    sudo systemctl status controlinfra-runner
  2. Check network connectivity:

    bash
    curl -v https://api.controlinfra.com/health
  3. View detailed logs:

    bash
    sudo journalctl -u controlinfra-runner -n 100 --no-pager

GCP Compute Engine Runner

Deploy a self-hosted runner on a Google Cloud Compute Engine instance using Service Account or Workload Identity for secure authentication.

Prerequisites

  • A Google Cloud project with billing enabled
  • A Compute Engine VM instance (Ubuntu 22.04 recommended)
  • Service Account or Workload Identity configured
  • Outbound internet access (HTTPS to Controlinfra)

Authentication Methods

MethodBest ForCredentials
Service AccountCross-project access, external VMsJSON key file
Workload IdentityGCE VMs in same projectAutomatic via metadata

Step 1: Create Service Account

Create a service account with the necessary permissions for drift detection:

bash
# Set your project
export PROJECT_ID="your-project-id"
gcloud config set project $PROJECT_ID

# Create service account
gcloud iam service-accounts create controlinfra-runner \
  --display-name="Controlinfra Runner" \
  --description="Service account for Controlinfra self-hosted runner"

# Get the service account email
export SA_EMAIL="controlinfra-runner@${PROJECT_ID}.iam.gserviceaccount.com"

Step 2: Assign Required Roles

The service account needs read access to your GCP resources and Terraform state:

bash
# Viewer role for drift detection (read access to all resources)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/viewer"

# Storage Object Viewer for Terraform state in GCS
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/storage.objectViewer"

# If using GCS for state with locking, also add:
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/storage.objectAdmin" \
  --condition=None

Required Permissions Summary

RolePurpose
roles/viewerRead access to GCP resources for drift detection
roles/storage.objectViewerRead Terraform state from GCS bucket
roles/storage.objectAdmin(Optional) If Terraform needs to write state

Step 3: Create Compute Engine VM

Option A: With Attached Service Account (Workload Identity)

bash
# Create VM with attached service account
gcloud compute instances create controlinfra-runner \
  --project=$PROJECT_ID \
  --zone=us-central1-a \
  --machine-type=e2-medium \
  --image-family=ubuntu-2204-lts \
  --image-project=ubuntu-os-cloud \
  --boot-disk-size=20GB \
  --service-account=$SA_EMAIL \
  --scopes=cloud-platform \
  --tags=controlinfra-runner

# Note the external IP for SSH access

Option B: Using Service Account Key

If using a key file instead of attached identity:

bash
# Create and download key file
gcloud iam service-accounts keys create ~/controlinfra-sa-key.json \
  --iam-account=$SA_EMAIL

# Securely copy to your VM
scp ~/controlinfra-sa-key.json user@VM_IP:~/

Step 4: Install Dependencies

SSH into your GCE instance and install required tools:

bash
#!/bin/bash

# Update system
sudo apt update && sudo apt upgrade -y

# Install Git and utilities
sudo apt install -y git curl jq

# Install Terraform
sudo apt install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \
  gpg --dearmor | \
  sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y terraform

# Install Google Cloud CLI (if not pre-installed)
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
gcloud init

# Verify installations
git --version
terraform --version
gcloud --version

Step 5: Register Runner in Controlinfra

  1. Go to SettingsRunners
  2. Click "Add Runner"
  3. Enter a name and add the gcp label
  4. Copy the registration token and runner ID

Step 6: Install Runner Agent

On your GCE instance, first install the Controlinfra CLI:

bash
# Install Controlinfra CLI
curl -fsSL https://controlinfra.com/cli/install.sh | bash

# Verify installation
controlinfra --version

Then authenticate and set up the runner:

bash
# Authenticate with your CLI token (from Settings > CLI Tokens)
controlinfra login --token YOUR_CLI_TOKEN

# Get the runner setup script (replace RUNNER_ID with your runner ID from Step 5)
controlinfra runners setup RUNNER_ID

The runners setup command will output an installation script. Run it to install the runner agent:

bash
# Example output - run the command shown by 'runners setup'
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=RUNNER_TOKEN" | sudo bash

Alternative: Direct Installation

If you already have the runner token from Step 5, you can install directly:

bash
curl -sL "https://api.controlinfra.com/api/runners/RUNNER_ID/setup?token=YOUR_RUNNER_TOKEN" | sudo bash

Step 7: Configure Authentication

Create the systemd service file at /etc/systemd/system/controlinfra-runner.service:

For Workload Identity (Attached Service Account):

ini
[Unit]
Description=Controlinfra Self-Hosted Runner
After=network.target

[Service]
Type=simple
User=controlinfra
WorkingDirectory=/opt/controlinfra-runner

# GCP Workload Identity - uses metadata server automatically
Environment="GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID"

ExecStart=/opt/controlinfra-runner/bin/controlinfra-agent
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

For Service Account Key:

ini
[Unit]
Description=Controlinfra Self-Hosted Runner
After=network.target

[Service]
Type=simple
User=controlinfra
WorkingDirectory=/opt/controlinfra-runner

# GCP Service Account authentication
Environment="GOOGLE_APPLICATION_CREDENTIALS=/opt/controlinfra-runner/gcp-sa-key.json"
Environment="GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID"

ExecStart=/opt/controlinfra-runner/bin/controlinfra-agent
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Workload Identity vs Service Account Key

Workload Identity (attached service account) is recommended for GCE VMs as it:

  • Requires no key management
  • Automatically rotates credentials
  • Uses the GCP metadata server for authentication

Service Account Key is useful when:

  • Running on non-GCP infrastructure
  • Needing cross-project access
  • Running in containers without metadata access

Enable and start the service:

bash
sudo systemctl daemon-reload
sudo systemctl enable controlinfra-runner
sudo systemctl start controlinfra-runner

Step 8: Verify Connection

  1. Check runner service status:

    bash
    sudo systemctl status controlinfra-runner
  2. View logs:

    bash
    sudo journalctl -u controlinfra-runner -f
  3. In Controlinfra, go to SettingsRunners - your runner should show as "Online"

Troubleshooting GCP Runner

"Could not find default credentials" Error

Error: google: could not find default credentials

Solution: Ensure either:

  • Service account is attached to the VM (Workload Identity)
  • GOOGLE_APPLICATION_CREDENTIALS points to valid key file

Check with:

bash
gcloud auth application-default print-access-token

"Permission Denied" on GCS State

Error: Failed to get state: googleapi: Error 403: Access denied

Solution: Ensure the service account has roles/storage.objectViewer on the state bucket:

bash
gsutil iam ch serviceAccount:${SA_EMAIL}:objectViewer gs://YOUR_STATE_BUCKET

"Caller does not have permission" Error

Error: googleapi: Error 403: Caller does not have permission

Solution: Add the roles/viewer role to the service account:

bash
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/viewer"

Runner Shows "Offline"

  1. Check the service is running:

    bash
    sudo systemctl status controlinfra-runner
  2. Check network connectivity:

    bash
    curl -v https://api.controlinfra.com/health
  3. Verify GCP authentication:

    bash
    gcloud auth list
    gcloud config get-value project
  4. View detailed logs:

    bash
    sudo journalctl -u controlinfra-runner -n 100 --no-pager

Metadata Server Not Accessible

Error: metadata: GCE metadata server not available

Solution: This error occurs when running outside GCE or in a container. Use a service account key file instead:

bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json"

Maintenance

Updating the Runner

bash
# Stop the runner
sudo systemctl stop controlinfra-runner

# Update
controlinfra-runner update

# Start the runner
sudo systemctl start controlinfra-runner

Rotating Tokens

  1. Generate new token in Controlinfra Settings
  2. Update runner configuration
  3. Restart the runner
  4. Revoke old token

Log Rotation

Configure log rotation in /etc/logrotate.d/controlinfra:

/var/log/controlinfra/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
}

Next Steps

AI-powered infrastructure drift detection