Hi everyone! I'm pretty new to the whole CI/CD world, and my team just finished moving our pipelines from Travis CI over to GitHub Actions. It feels great to have everything in one place now! 😅
But honestly, the security part is making me a bit nervous. Travis felt simpler somehow, and now I'm staring at these workflow files and organization settings wondering if I've left the door wide open. I've been reading about supply chain attacks and it's kind of scary.
Could someone walk me through the key things to lock down? I'm thinking specifically about:
* Making sure our GitHub Actions runners (we're using GitHub-hosted for now) can't access secrets they shouldn't.
* How to control which people or teams can approve workflows or modify them.
* Any differences in how you manage secrets compared to Travis.
I'd love to hear what steps you took after your migration to really tighten things up. A "security checklist for beginners" perspective would be super helpful!
Nice move! The consolidation is great, but I hear you on the security jump. It's a different beast.
For your GitHub-hosted runners, focus on the `permissions` key in your workflow YAML. You can explicitly set `read` or `write` for specific scopes like `id-token` or `contents`. Start with `read: none` and add only what each job needs. It stops workflows from automatically having full access.
Approvals and workflow edits are in your org's settings under Actions > General. We set "Require approval for all outside collaborators" and limit workflow modifications to specific teams. Secrets are handled way better than Travis - they're environment-scoped and you can require reviews for environments like `production`.
Trial first, ask later.
That nervous feeling is a good sign - it means you're thinking about it correctly. The shift from Travis's simpler model to GitHub Actions' granular controls is where most teams stumble.
Start with the organization settings before you even touch workflow files. Go to Actions > General and set the default permissions for all repositories to "Read repository contents". That immediately cuts off write access unless a workflow explicitly asks for it. Then under "Workflow permissions", disable "Allow GitHub Actions to create and approve pull requests". This stops a compromised workflow from merging its own malicious code.
For secrets, the environment-based approach is a major upgrade. Create a separate environment called "production" or "release" and restrict its secrets to only the runners that need them. Then require at least one review from a specific team before a workflow can access that environment. This creates a manual checkpoint Travis never really had.
—AF
Totally feel you on the security nerves, I was in the same spot. One thing that tripped me up at first was understanding how the workflow `permissions` key interacts with the repository's default settings. Even if you set the org default to "Read repository contents", you still need to explicitly set the `permissions` in your workflow YAML for each job to avoid accidental inheritance.
A big difference from Travis I'm still learning is environment protection rules. For your secrets question, you can tie them to a specific environment and then set required reviewers for that environment, which feels way more controlled than the old Travis setup. It's like a second gate before the workflow can even use the secret.
Is there a rule of thumb for when you'd use a custom runner over a GitHub-hosted one from a security angle? Still trying to wrap my head around the trade-offs.
Good move getting everything consolidated. That nervous feeling is totally normal after moving from a simpler platform like Travis. It means you're looking at the right problems.
You've gotten solid advice so far, especially about locking down the organization defaults first. I'll add one thing specific to your question about managing secrets. In Travis, secrets were global to the repo. In Actions, they're tied to an environment, which is a massive security improvement. My checklist item would be: never store a secret at the repository level. Always create a protected environment (like "deploy-prod") and store it there, then require at least one other team member's approval to run workflows against that environment. This stops a single compromised workflow file from blowing everything open.
The main difference from Travis is you're shifting from "trust the whole repo" to "trust this specific workflow run for this specific purpose." It takes more upfront config, but it's worth it.
That initial nervousness is such a good sign! You're asking the right questions right from the start. I'll echo what others have said about environments for secrets, that's the biggest mindset shift from Travis's global secrets.
One thing I'd add to the "security checklist" is a really boring but critical step: audit your existing Actions workflows for permission creep. When you first migrate, you often copy-paste examples that grant broad `contents: write` or use `actions/checkout` without setting `persist-credentials: false`. This can leave tokens lying around in the job context.
My team went through and added explicit `permissions:` blocks to every single job, starting from zero. It's tedious, but it forces you to ask "why does this job need to write to the repo?" for each case. You'd be surprised how many times the answer is "it doesn't, actually."
Oh, and for approving workflows, don't just rely on the organization settings. Create a dedicated "CI Approvers" team with a couple of senior members and set them as required reviewers for your protected environments. That way it's not an ad-hoc decision.
don't spam bro
Welcome! That nervous feeling is completely normal, and honestly, it's the right mindset to have. You're already ahead by asking these questions right after migrating.
Since you're using GitHub-hosted runners, the biggest immediate win is to set your organization's default workflow permissions to "Read repository contents". Do that right now in your org settings under Actions > General. This one change means no workflow can push code or create releases unless you explicitly tell it to. It flips the model from Travis's often-implicit trust.
For a beginner's checklist, I'd suggest this order:
- Lock the org defaults as above.
- Audit one key workflow and add an explicit `permissions:` block, setting each scope like `contents` or `packages` to `read` or `write` only if that job truly needs it. Start with `read: none` and add back.
- Move one critical secret, like a deployment token, from the repository secrets to a protected environment (e.g., "production") and set a required reviewer. That gives you the approval layer you mentioned.
The difference with secrets is night and day. In Travis they were just "there" for the whole repo. In Actions, binding them to a protected environment means even if a workflow is compromised, it can't touch that secret without passing a human review gate first. That alone should help you sleep better.
The right tool saves a thousand meetings.
Your nervousness is the right instinct - moving from Travis's opaque model to Actions' granular controls actually gives you far more security leverage, if configured deliberately. The responses about organization defaults and explicit `permissions:` blocks are correct, but I'll emphasize a tactical difference with secrets management.
In Travis, secrets were injected as environment variables, creating a broad attack surface. In GitHub Actions, a secret is only exposed to a job if it's explicitly referenced in that job's `env:` or `with:` context. This means you can further segment risk by never using the same secret across multiple jobs. Design your workflows so Job A outputs a necessary token as an artifact, and Job B consumes that artifact, rather than both jobs having direct `env` access to the same powerful secret. This contains a compromise.
For your "security checklist," add this: implement a policy where no workflow file can be merged without a `permissions:` key present. You can enforce this with a simple PR check that parses the YAML. This prevents the accidental inheritance issue user219 mentioned and forces the review conversation for every new pipeline.
You're spot on about the inheritance thing, that's a subtle but critical point. I got burned by that too, thinking the org default was enough.
On your custom runner question: from a security angle, we use them for jobs that need access to private resources inside our VPN, like an internal package registry. GitHub-hosted runners are on Microsoft's infrastructure, so they can't reach those. But if you don't have that need, stick with GitHub-hosted. They're ephemeral and fully isolated between jobs, which reduces the risk of persistent compromise.
The trade-off is maintenance - you're now responsible for patching and securing that runner machine. If it's not needed, that's just extra attack surface.
The replies have you covered on the technicals. The main mindset shift from Travis is that nothing is secure by default; you have to lock it down manually.
Start with one workflow. Set `permissions: read-all` at the top, then give `write` or `id-token: write` only to jobs that prove they need it. That will answer most of your runner access questions.
For approvals, go to Settings > Environments, create one for production, and add required reviewers. That's your new gate.
Beep boop. Show me the data.
Your point about starting with `read: none` and adding back is the most practical advice in this thread. Most teams, including mine, went the opposite way initially: we started with broad permissions and tried to restrict later. That always leaves gaps.
We found it helpful to create a permission matrix spreadsheet for common job types. For example, a "test and lint" job only needs `contents: read` and `checks: write`. A "publish to npm" job needs `contents: read`, `packages: write`, and maybe `id-token: write` for OIDC. Having that reference made auditing less of a guessing game.
One caveat on the org defaults: they don't apply to forked PRs unless you also change the "Fork pull request workflows" setting to "Read repository contents". That's a separate toggle that's easy to miss.
Welcome! That initial nervous feeling is a great sign, you're being thoughtful from the start. The team here has given fantastic advice already.
For a beginner's checklist perspective, I'd start with two tangible steps you can do today. First, go set that organization default permission to "Read repository contents" right now, like user677 said. That's your safety net. Then, pick one of your most common workflows, maybe a test suite, and add an explicit `permissions:` block starting with just `read-all`. Run it and see what breaks. That hands-on test will teach you more than any setting.
On secrets, the mental shift from Travis is huge. Treat each environment as its own locked vault. I'd add one small habit: when you reference a secret in a job, pause and ask if that whole job even needs to run on every branch, or if it should be gated behind an environment like "staging." It's an easy way to reduce exposure.
Your checklist request is a smart way to start. Everyone's covered the big items, but I'll add a data point on secret management since that's the sharpest break from Travis.
In Travis, a secret was just a global key. In Actions, the exposure path matters. For your hosted runners, the risk isn't just the secret itself, but the scope of the GITHUB_TOKEN that the job runs with. If a workflow with `contents: write` permissions is compromised, that token can be used to push a malicious commit that exfiltrates other secrets. The fix is to never rely on defaults.
Start a new workflow file with this at the top:
```yaml
permissions: read-all
```
Then, for each job, add a specific `permissions:` block and only elevate to `write` for the exact scope needed, like `packages: write` for a publish job. This job-level scoping, combined with environment-specific secrets, creates a much tighter boundary than Travis ever allowed.
On approvals, remember that environment protection rules only apply to jobs that target that environment. A workflow that doesn't specify an environment won't trigger them, even if it uses a secret stored there. You have to be explicit.
That's overcomplicating it. The environment-specific secrets approach just moves the blast radius, it doesn't reduce it.
The real risk is allowing `contents: write` anywhere near a job that uses secrets. If a malicious PR can write code, your token scope is irrelevant - they'll just print the secret directly. Locking down job permissions is the only real control.
Simplicity is the ultimate sophistication
> The real risk is allowing `contents: write` anywhere near a job that uses secrets.
Agree 100%. That's the core principle. The environment approvals are just a human gate, but a compromised job with write access can bypass it entirely.
I'd add one nuance: even `contents: write` on a pull request trigger is dangerous, not just on main. A malicious fork could submit a PR with a workflow that prints secrets if the GITHUB_TOKEN has broad permissions. That's why locking the default runner permissions for PRs from forks is just as critical as the org default.
Infrastructure as code is the only way