Development & Building with AI · Tutorial
How to set up automated deployment securely
Automating deployment takes human error out of the process, but creates a new target: the pipeline has access to production. See how to build an automated deployment that only ships what passed the tests and keeps the keys safe.
- Author:
- Redação Token & Tech
- Responsible editor:
- Daniel Ferreira
- Translation:
- Redação Token & Tech
Published · 7 min read
Originally published in Brazilian Portuguese.
Translated from the Portuguese original with AI assistance.
Also available in: Português
Key points
- Automated deployment is only secure if a mandatory check comes first: a build and tests that actually fail stop the release.
- Credentials stay out of the repository, with the least access possible; when the provider allows it, swap long-lived keys for short-lived tokens via OIDC.
- Protect the path to production: minimal permissions, an environment with approval, actions pinned by commit and a rehearsed rollback.
Deploying by hand has a well-known problem: each time is a little different. Someone forgets to run the tests, copies the wrong folder or uploads to another project’s server. Automated deployment, in which a process publishes the site without manual intervention after each approved change, solves that.
But it creates another risk. The pipeline, the automated sequence of steps that goes from code to production, now holds the server’s keys. Whoever controls the pipeline controls the site. OWASP, a foundation dedicated to software security, maintains a specific list of the top ten CI/CD security risks, which includes insufficient flow control, insufficient credential hygiene and poisoned pipeline execution.
This tutorial uses GitHub Actions as an example, because its security documentation is detailed. The principles apply to other tools. At the end, there is the case of Token & Tech itself.
Step 1: set the gate before deployment
The first rule of automated deployment is that it only publishes what passed a check. The order is always the same: build, tests, and only then deployment. If any step fails, the process stops.
This requires tests that actually fail, with a non-zero exit code. A test that only prints warnings blocks nothing. For websites, the minimum check includes internal links and SEO metadata, the subjects of How to test internal links and routes automatically and How to automate technical SEO audits.
In GitHub Actions, split verification and deployment into two jobs, with the second depending on the first. That way, deployment does not even start if verification fails.
Step 2: get credentials out of the repository
No password, token or server key should be in the code. GitHub’s security reference is direct: sensitive data should never be stored as plain text in workflow files.
Use the platform’s secrets store. On GitHub, secrets are referenced as environment variables of the step that needs them. The documentation on using secrets recommends avoiding passing secrets as command-line arguments, which may be visible to other processes or in audit logs. Prefer an environment variable or standard input.
Other recommendations from the same documentation:
- Create one secret for each value. Do not store an entire JSON or YAML block in a single secret, because masking in logs depends on an exact match.
- Give each credential only the access it needs and rotate them periodically.
- Workflows triggered by pull requests from forks (copies of the repository made by third parties) do not receive secrets, except for
GITHUB_TOKEN.
To prevent a key from reaching the repository by mistake, GitHub has secret scanning push protection: it blocks pushes of commits containing recognized credentials before they reach the repository (documentation). In private repositories, the feature depends on GitHub Secret Protection. A .gitignore that excludes .env and local configuration files is the first barrier, and it is free.
Step 3: prefer short-lived credentials
A secret kept in the store is still a long-lived key. If it leaks, it remains valid until someone revokes it.
When the target is a compatible cloud provider, GitHub Actions lets you use OIDC (OpenID Connect), an identity protocol. Instead of storing the cloud key, you configure a trust relationship with GitHub at the provider. On each run, the workflow requests a token that, according to the documentation, is valid for a single job and expires automatically. The provider checks which repository and which environment the request came from before granting access.
For this, the job needs the id-token: write permission (provider configuration). Not every host supports OIDC. When it does not, use a credential dedicated to deployment, restricted to one folder or one repository, and never the main account password.
Step 4: reduce workflow permissions
Each GitHub Actions run receives an automatic token, GITHUB_TOKEN. The recommendation in the security reference is to set the default permission to read-only and expand it only in the job that needs it.
In the workflow syntax, when you declare any permission, all undeclared ones become none. An example structure, for illustration only:
# example: .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: production
cancel-in-progress: false
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-commit-SHA>
- run: npm ci
- run: npm run build
- run: npm run check
deploy:
needs: verify
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write # only if the target supports OIDC
steps:
- run: echo "deploy here, with the environment's credential"
The example omits how the build output passes from one job to the other; in a real project, you rebuild in the deploy job or use artifacts.
Three details of this file:
concurrencyprevents two simultaneous deployments. With a group defined, the documentation states that only one run stays pending per group; a new pending run cancels the previous one. Thecancel-in-progress: trueoption also cancels the one currently running. For deployment, the newsroom prefersfalse, so as not to interrupt a release midway.- Actions pinned by SHA. Third-party actions referenced by tag, such as
@v4, may have the tag moved. According to GitHub, pinning the action to the full commit SHA is “currently the only way” to use it as an immutable release. - Untrusted input. Pull request titles, branch names and commit messages are written by whoever opens the change. Inserted directly into a
run:command, they can inject code. The documentation recommends passing them through an intermediate environment variable.
Step 5: protect the production environment
On GitHub, an environment is a deployment target with its own rules, such as production or staging. The job declares it with the environment key and waits for the rules to be met before running (deployment documentation). The environments documentation describes the protections:
- Required reviewers: up to six people or teams; one approval is enough. There is an option to prevent whoever triggered the run from approving themselves.
- Allowed branches: only the chosen branches or tags can deploy to that environment.
- Wait timer: a delay before the job proceeds.
- Environment secrets: only available to the job after the rules are met.
By default, administrators can bypass these rules; the option can be turned off. There is a plan limit: on GitHub Free, environments can only be configured in public repositories. Private repositories require GitHub Pro or Team. In small teams, combining a protected branch with a secret restricted to the environment already closes much of the path.
Two precautions with triggers, also from the security reference: avoid self-hosted runners (your own machines that run the jobs) in public repositories, and do not use pull_request_target to run untrusted pull request code, because this trigger has write access and access to secrets.
Step 6: verify afterward and have a rollback
A deployment finishing without errors does not prove the site is up. Include a post-deployment check: a few requests to the real address checking HTTP status, redirects and a recent page.
And decide in advance how to roll back. A rollback is restoring the previous version. If each deployment is a commit in a history, rolling back means reverting the commit and deploying again. The procedure should be written down and rehearsed; figuring out how to undo things during an outage is the worst time to do it.
Documented case: how Token & Tech is deployed
The site you are reading uses a simple version of these principles, without GitHub Actions: a deployment script run locally, scripts/publish.sh, described in the project’s DEPLOY.md.
- Gate before deployment. The script runs the build and the
npm run checkverifier. If the check fails, it rebuilds once, because articles may be published during the build, and stops for good if it fails again. - Separate branch for deployment. The source code lives on one branch; the generated site goes to another, dedicated to deployment, which contains only the final files. The host pulls this branch automatically. The source code is never published.
- Target check. Before pushing, the script checks that the working folder points to the expected remote repository and stops if it does not. This avoids publishing to the wrong project.
- No credentials in the repository. The deployment document contains no passwords,
.gitignoreexcludes.envand server configuration files, and the verifier fails if any of these files appears in the published folder. - Remote verification and rollback. The procedure lists checks on the live site after deployment, such as redirects, main pages, sitemap and the 404 error page. Rollback means reverting the last commit on the deploy branch, which makes the host republish the previous version.
One limitation: because the script runs on the machine of whoever deploys, there is no mandatory approval or centralized record of runs, which a protected environment on a CI service would offer.
Key takeaways
- A build and tests that actually fail come before any deployment.
- Credentials stay in the secrets store, never in the code; when possible, use short-lived tokens via OIDC.
- Least privilege by default, actions pinned by SHA and external input treated as untrusted.
- Protect production with a restricted environment and branch, check the site after deploying and keep a rehearsed rollback.
Disclosure: The Token & Tech website uses the techniques described here.
Sources
- Concurrency, GitHub Docs. Accessed October 8, 2026
- Deploying with GitHub Actions, GitHub Docs. Accessed October 8, 2026
- Managing environments for deployment, GitHub Docs. Accessed October 8, 2026
- Configuring OpenID Connect in cloud providers, GitHub Docs. Accessed October 8, 2026
- OpenID Connect, GitHub Docs. Accessed October 8, 2026
- About push protection, GitHub Docs. Accessed October 8, 2026
- Using secrets in GitHub Actions, GitHub Docs. Accessed October 8, 2026
- Secure use reference (GitHub Actions), GitHub Docs. Accessed October 8, 2026
- Workflow syntax for GitHub Actions, GitHub Docs. Accessed October 8, 2026
- OWASP Top 10 CI/CD Security Risks, OWASP Foundation. Accessed October 8, 2026