quickstart →

Security: what runs in your account, and what RunsOn receives

Where your code runs, what RunsOn receives, how jobs are kept apart, what the GitHub App can do, and which settings to change.

RunsOn runs in your AWS account. Your code, your secrets and your logs are never sent to RunsOn.

  • One job, one machine. Every job runs on an EC2 instance that no other job has used. The instance is terminated when the job ends.
  • Nothing shared with other customers. The control plane and the runners are yours. RunsOn has no access to your AWS account or your GitHub organization.
  • Two requests to RunsOn. A license check, and one event per job. Neither contains code, secrets or logs.

This holds for both Flex and Fleet. They differ in how jobs reach your account.

What RunsOn receives#

Two HTTPS requests go from your account to runs-on.com. RunsOn receives nothing else: no crash reports, no update checks, and no connection from RunsOn into your account.

License check#

The control plane sends it 90 seconds after it starts, then every 72 hours.

{
"org": "your-github-org",
"region": "us-east-1",
"license_key": "...",
"app_version": "v3.4.0"
}

org is your GitHub organization, or your enterprise slug for Fleet in enterprise mode.

Job event#

The agent on the runner sends it once, when the job starts. RunsOn uses it to count jobs for licensing and to see which versions and features are in use. It cannot be turned off. These are all its fields:

  • org: the GitHub organization that owns the repository.
  • license_key_hash: a SHA-256 hash of your license key.
  • stack_name, environment: the names you gave the stack and its environment.
  • product, deployment_method: flex or fleet, and how it was deployed, such as cloudformation.
  • version, region: the RunsOn version and the AWS region.
  • platform, architecture: the operating system and CPU architecture.
  • instance_type, instance_lifecycle: the EC2 instance type, and spot or on-demand.
  • timings: the name and time of each boot step.
  • is_private, is_ghes: whether the runner has no public IP, and whether you use GitHub Enterprise Server.
  • networking_stack: how the network was set up (embedded, external, or a stack name).
  • integrations_active, extras: the integrations and extras that are enabled.
  • pool_name, pool_standby_type: for a warm pool runner, the pool’s name, and hot or stopped.
  • features, from v3.5: the image OS; on or off for SSH, lifecycle hooks, OpenTelemetry and the cache broker; the sticky disk’s size, type, IOPS and throughput.
  • sent_at: when it was sent.

stack_name, environment and pool_name are names you chose.

The event never contains a repository, workflow or job name, a run ID, your AWS account ID, code, secrets, logs, or files from the runner.

If runs-on.com were compromised, an attacker would get this data. Both requests go out from your account, so there is nothing to follow back in.

See also the Privacy Notice and the subprocessors.

How jobs are kept apart#

Every job gets:

  • Its own instance. No other job has run on it, and it is terminated when the job ends. Warm pool instances are started ahead of time, and still run one job each.
  • A single-use registration. The runner joins GitHub with a just-in-time configuration that is valid for one job. Only that instance can read it, and it is deleted after a day.
  • No way to the GitHub credentials. The runner’s IAM role cannot read Secrets Manager. Only the control plane can.
  • No inbound traffic. The runner’s security group has no inbound rule, unless you allow SSH.
  • IMDSv2 only. Instance metadata requires a session token.

Several repositories on one stack#

Repositories that use the same stack share its cache bucket and its runner IAM role. Two things keep them apart:

  • Turn on cache isolation. We recommend it on every stack. Each job then gets cache credentials for its own repository and branch, with the same rules as GitHub’s own cache: a job reads and writes its branch, and can read the default branch. Set EnableCacheIsolation for Flex, or enable_cache_isolation for Fleet. Each repository starts with an empty cache once, when you turn it on.
  • Give untrusted code its own stack. Cache isolation covers the GitHub Actions cache: actions/cache, the setup actions and Docker’s type=gha backend. Tools that write to S3 with the runner role, such as sccache, stay shared across the stack. For public repositories, use a separate environment.

How jobs reach your account#

This is the one place where Flex and Fleet differ.

FlexFleet
How jobs arriveGitHub posts webhooks to your stackfleetd pulls them from GitHub
Public endpointAPI Gateway and LambdaNone
Repositories that can use runnersThose the GitHub App is installed onThose in the GitHub runner group
What a workflow can chooseInstance type, image, disk and more, with job labelsA runner fleet, by name
Flex
  • The endpoint is public, because GitHub has to reach it. The control plane checks GitHub’s signature on every webhook before it acts on it.
  • Limit it to GitHub’s IP ranges with EnableWAF=true. This works with github.com only, not with GHE.com or GitHub Enterprise Server. The stack rejects EnableWAF=true when GithubEnterpriseUrl is set.
  • Remove the setup pages once RunsOn is installed, with EnableAdminRoutes=false. They are served from the same endpoint.
  • Labels stay inside the stack’s settings. A workflow can ask for a bigger instance or another image. It cannot turn on SSH when SSHAllowed=false, or get a public runner when Private=only.
Fleet
  • There is no endpoint to protect. fleetd and the runners only make outbound connections.
  • Runner groups decide who gets in. A GitHub runner group lists the repositories that can use a runner fleet. Use separate groups for different access rules, and a separate stack for different IAM permissions or networks.
  • Runner settings are out of reach of workflows. Instance type, image and disk are set in Terraform. A workflow only names the runner fleet.

What the GitHub App can do#

RunsOn talks to GitHub through an app you create in your own organization. The app is private. Its key is stored in AWS Secrets Manager in your account, and RunsOn never sees it.

Flex
PermissionLevelAccess
AdministrationRepositoryRead and write
ActionsRepositoryRead and write
Single fileRepositoryRead and write, .github/runs-on.yml only
MetadataRepositoryRead
DeploymentsRepositoryRead
MembersOrganizationRead
  • Five of the six are repository permissions. They apply only to the repositories the app is installed on. The app has no organization administration permission. Members only lets it read who is in the organization.
  • It cannot read your code. The app has no Contents permission. The one file it can read or write is .github/runs-on.yml, the repository’s RunsOn configuration.
  • Administration is the broad one. This is repository administration, which GitHub requires to register a runner on a repository. Limit who can read the app’s secret in Secrets Manager, and install the app only on the repositories that use RunsOn.
Fleet
  • Organization mode: a GitHub App with two permissions: organization self-hosted runners (read and write), and Actions (read and write), which Fleet uses to re-run jobs after a spot interruption. It cannot read your code.
  • Enterprise mode: a classic personal access token with the manage_runners:enterprise scope. It is a token only because GitHub does not yet let a GitHub App manage enterprise runners. Re-running jobs after a spot interruption also needs the repo scope, which gives the token access to repositories.
  • Both are Terraform inputs, so they are also in your Terraform state. Keep the state encrypted, and limit who can read it.

See the Fleet installation guide.

What you have to trust#

Four pieces of RunsOn software run in your account.

ComponentWhere it comes fromHow to check or replace it
Control planeA Docker image from RunsOn’s public ECR registry, run on ECS FargateReview the source, or build your own image and run it from a private ECR repository
AgentA binary that runners fetch from your own S3 bucketReview the source, or rebuild it
BootstrapA small binary that each runner downloads at boot from the runs-on/bootstrap ↗ releasesThe source is public
Runner imagesPublic AMIs built from the public runner-images-for-aws ↗ repositoryBuild from the same templates, or use your own custom AMIs
  • Source access. The CloudFormation template and the Terraform modules are public. The control plane and the agent are proprietary: their full source comes with the Sponsorship license, for your own security review.
  • Image updates. RunsOn rebuilds its AMIs within 15 days of each GitHub release, and scans the published AMIs every night with Amazon Inspector.
  • Upgrades are yours. Your stack pins the control plane version. It changes when you upgrade, and not before.

Settings to review#

Out of the box:

  • On: webhook signature checks, Session Manager access to runners, and the Flex setup pages.
  • Off: SSH, WAF, cache isolation, sticky-disk isolation and disk encryption.

These are the settings to change:

Flex

These are the CloudFormation parameters. With Terraform, use the matching inputs in the Flex Terraform reference.

Fleet

Fleet has no public endpoint, so there is no WAF and there are no setup pages to remove.

By default, Flex control-plane SQS queues use SQS-managed server-side encryption. The SNS alerts topic has no server-side encryption and relies on SNS’s own disk encryption. Starting with v3.5.0, you can encrypt the queues and the SNS alerts topic with your own KMS key. Set messaging_kms_key_arn in the Terraform module (Flex or Fleet; Fleet has only the topic). It takes a key ARN, not an alias.

The module grants the RunsOn roles kms:Decrypt and kms:GenerateDataKey on the key through IAM. The key policy must therefore delegate to IAM in your account. Without that, the RunsOn roles can’t use the key: Flex can’t send or receive queue messages, and alerts can’t be published.

The key policy must also allow the AWS services that publish to the queues and the topic:

{
"Sid": "AllowRunsOnServicePublishers",
"Effect": "Allow",
"Principal": { "Service": ["events.amazonaws.com", "budgets.amazonaws.com"] },
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "*",
"Condition": { "StringEquals": { "aws:SourceAccount": "<account-id>" } }
}
  • events.amazonaws.com is needed on Flex only. Without it, Spot interruption and instance-state events are dropped. Fleet has no SQS queues, so leave it out there.
  • budgets.amazonaws.com is needed on Flex and Fleet whenever app_budget_daily_usd is above 0. It defaults to 10 on both. Without it, budget alerts don’t reach the topic.

The AWS-managed alias/aws/sqs and alias/aws/sns keys can’t be used, because their key policies don’t allow these services.

Two notes on the defaults:

  • Runner disks are not encrypted by default, because encryption adds about 10 seconds to every boot.
  • Runners are in public subnets by default, with a public IP and no inbound rule. Networking covers private subnets and static egress IPs.

RunsOn does not change what a job’s GITHUB_TOKEN can do. Give each workflow only the permissions: it needs.

To watch or restrict what a job does while it runs (network egress, file changes), see the StepSecurity integration.

Report a vulnerability#

Email security@runs-on.com. The vulnerability disclosure page lists what to include, the rules for testing and the safe harbor.

For a security questionnaire, or a question about a specific data flow, write to ops@runs-on.com.