Deploying and securing with GitHub Actions
CI checks code. CD ships it, and to ship it the workflow needs a key to your servers or cloud. This lesson shows how to deploy through staging and an approval gate to production, how to give a workflow short-lived access instead of a stored key, and the handful of settings that stop a workflow from becoming the easiest way to break into your systems.
By the end you can
- Build a deploy workflow with staging, a required approval, and production
- Explain OIDC: how a job gets a short-lived cloud token without any stored key
- Set
permissions:to least privilege and pin actions by commit SHA - Turn on secret scanning, push protection, Dependabot and CodeQL, and know what each catches
- Spot a dangerous
pull_request_targetworkflow, and review a third-party action before using it
The everyday picture
In a large store the cash room has one door and one key. The store manager holds it. A cashier who needs change cannot walk in: she asks, the manager opens the door, and the opening goes in the register with the time and the reason. The key never leaves the manager's ring, nobody gets a copy "to save time", and if the manager changes, the lock is changed.
A deploy workflow is the cashier who needs to get into the cash room: your production servers or your cloud account. This lesson is about the door. Who may open it (environments and approvals), whether there is a copy of the key lying around (stored secrets versus short-lived tokens), how much each person can touch once inside (permissions), and the register (deployment history and audit logs).
From CI to CD: environments and protection rules
Continuous delivery (CD) takes the build that CI produced and releases it. A good CD workflow builds once, saves the result as an artifact (see GitHub Actions: continuous integration), and deploys that same artifact to staging and then to production. Building again for production would mean production runs something nobody tested.
In GitHub Actions, the gate is an environment. You create it under Settings, Environments, and a job joins it with environment: production. The environment can have:
- Required reviewers: up to six people or teams. The job pauses with "Waiting for review" until one of them clicks Approve and deploy. Tick Prevent self-review so the person who pushed cannot approve their own deploy.
- Wait timer: a delay of 1 to 43,200 minutes (30 days) before the job may start, for example 15 minutes to watch staging metrics.
- Deployment branches and tags: only
main, or only tags likev*, may deploy here. A workflow on a feature branch that saysenvironment: productionis refused. - Environment secrets and variables, which a job can read only after the protection rules pass. The production SSH key is not even visible to the job until a reviewer has approved it.
Every deploy is recorded on the repository's Deployments page (in the repository sidebar) with the environment, the commit, the source PR, the status and a link to the run, and the approval with the reviewer's comment appears in that run: together they are the cash-room register. A job can wait at most 30 days for approval. Plans matter here: environments with secrets work in private repositories on Pro, Team and Enterprise, but required reviewers and wait timers on private repositories need GitHub Enterprise; on Free, Pro and Team they work only in public repositories. Check your organisation's plan before you design the process around an approval button.
Quick check
The production environment allows deployments from main only. A developer pushes a workflow on feature/fast-deploy whose job says environment: production. What happens?
Deployment branch rules are checked before anything else: a branch that is not allowed cannot deploy to the environment at all, so its secrets stay locked. Naming an environment in YAML is a request, not a permission, and a reviewer cannot override a branch rule.