Migrating from Travis CI to GitHub Actions often focuses on workflow syntax translation, but a critical and frequently overlooked post-migration phase is tightening the security model. Travis operated with a relatively flat permissions structure within a repository context, whereas GitHub Actions introduces a more complex and powerful matrix of permissions, secrets, and runner types. Failing to adjust these defaults leaves you with the "Travis mindset" on a more permissive platform. This guide details the systematic steps I took to lock down our organization's setup, benchmarked against our previous Travis configuration for a quantifiable security improvement.
The primary attack surface in GitHub Actions consists of three vectors: repository permissions granted to the GITHUB_TOKEN, organization and repository secrets, and self-hosted runner management. Here is the breakdown of the remediation process.
**1. Restricting the Default GITHUB_TOKEN Permissions**
The default `GITHUB_TOKEN` has write access to almost all scopes. The first step is to enforce least-privilege at the organization or repository level. In `.github/settings.yml` or via Organization Settings > Actions > General, set the default permissions to be minimal.
```yaml
# .github/settings.yml (if using the Settings repository)
repository:
name: "your-repo"
default_permissions:
contents: read
issues: read
checks: write # Only if you need to update check statuses
deployments: none
packages: none
```
For finer control, explicitly set permissions in each workflow job, overriding any defaults. This is the most secure pattern.
```yaml
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write # Required for OIDC auth to cloud providers
steps:
- uses: actions/checkout@v4
```
**2. Migrating and Scoping Secrets**
Travis secrets were environment-specific. In GitHub Actions, secrets can be set at repository, environment, or organization level. We migrated all secrets and then applied them strictly.
* **Repository secrets:** Used for general build dependencies (e.g., `NPM_TOKEN`).
* **Environment secrets:** Critical for deployment targets (e.g., `PROD_AWS_CREDENTIALS`). These require explicit approval workflows when referenced. Configure required reviewers for the `production` environment in repository settings.
* **Organization secrets:** Reserved for internal registry tokens shared across many repos. Use caution.
**3. Securing Self-Hosted Runners (The Largest Risk)**
Travis's isolated, ephemeral VMs were a security feature. GitHub self-hosted runners, if not configured correctly, are persistent and can be compromised by a malicious pull request. Our policy:
* **Never use org/enterprise-level runners for public repositories.** This is a catastrophic risk.
* **Use repo-level runners** for sensitive projects, and label them specifically (`self-hosted`, `linux`, `arm64`).
* **Implement ephemeral runner patterns** using tools like `actions-runner-controller` on Kubernetes, or configure the runner application to run once (`--ephemeral`). Our benchmark showed a 15% latency overhead versus persistent runners, a acceptable trade-off for security.
* **Harden the runner host OS:** Disable SSH, use read-only filesystems where possible, and apply strict network policies.
**Timeline and Metrics**
The full audit and remediation for a mid-sized organization (120 repositories) took approximately 18 engineer-hours over two weeks. The process was broken down into:
1. Inventory and risk assessment (4 hours).
2. Organization-wide default token permission change (1 hour, including communication).
3. Secret migration and environment setup (6 hours).
4. Self-hosted runner policy implementation and testing (7 hours).
The outcome is a GitHub Actions configuration that is materially more secure than our former Travis CI setup, primarily due to the granular permission model and the ability to enforce approval gates via environments. The main ongoing cost is the slight complexity added to workflow files, requiring explicit `permissions:` blocks, but this is a mandatory tax for a secure CI/CD system.
benchmarks or bust
Oh, the glorious "quantifiable security improvement." I've sat in meetings where that phrase was uttered right before a quantifiable data loss event. Your vector analysis is sound, but the devil is in the timeline.
You can set all those lovely restrictive defaults in the organization settings today, but they only apply to *new* workflows. Every existing workflow file, with its inherited Travis-era "write-all" mindset, will continue to run with the old, broad permissions until someone manually edits each one. That's the trap. You think you've locked the door, but you've only changed the lock on the door to the empty room next door.
We discovered this six months post-migration when an old, forgotten "deploy preview" workflow, untouched since the translation, suddenly had permission to write to a protected branch because the org-level change never touched it. The fix isn't just setting policies; it's a grueling, manual audit of every single `.github/workflows` directory. Did you account for that in your remediation process? The gap between policy and practice is where the real breaches live.
Test your rollback first
The GITHUB_TOKEN write-access default is a genuine latency multiplier. Our team observed a 15-20% increase in workflow duration after switching to read-only, traced to unnecessary artifact uploads and status checks we'd inherited from Travis. Your point about `.github/settings.yml` is correct, but the operational overhead of managing that file across hundreds of repositories led us to enforce it via the organization API. We scheduled a one-time script to audit and adjust all existing repos, because as you noted, the new defaults don't apply retroactively. The performance penalty was a temporary tax for eliminating that vector.
--perf