| 1 | --- |
| 2 | title: Secrets and variables |
| 3 | description: One list of keys and values for workflows and deployments. Each row says which environments it applies to and who reads it. |
| 4 | --- |
| 5 | |
| 6 | A [project](/guides/projects/) and a workspace each have one list of |
| 7 | secrets and variables. |
| 8 | Workflows and deployments both read from it; each row says whether one, |
| 9 | the other or both do, and which environments it applies to. |
| 10 | |
| 11 | | Where | Page | Who sees and changes it | |
| 12 | | --- | --- | --- | |
| 13 | | A project | **Settings → Secrets and variables**, `g1t.sh/<workspace>/<project>/settings/secrets` | People with the Admin [role](/guides/access-and-roles/) on its repository | |
| 14 | | A workspace | **Settings → Secrets and variables**, `g1t.sh/<workspace>/-/secrets` | Owners | |
| 15 | |
| 16 | Seeing the list needs the same as changing it: a project's rows, config |
| 17 | values included, are for Admins of its repository, and the page is not |
| 18 | shown to anyone else. |
| 19 | |
| 20 | A workflow that deploys to AWS, Google Cloud or Azure needs no cloud key |
| 21 | here: it can sign in with the job's short-lived |
| 22 | [OIDC token](/guides/actions/#oidc-tokens) instead. |
| 23 | |
| 24 | ## A row |
| 25 | |
| 26 | | Field | | |
| 27 | | --- | --- | |
| 28 | | **Type** | **Secret**: sealed when saved and never shown again, hidden in logs. For passwords, API keys and tokens. **Config**: readable by whoever may see the list. For values that are not sensitive. Config can be changed to a secret; a secret can never become config. | |
| 29 | | **Key** | Letters, digits and underscores, not starting with a digit, upper-cased: `STRIPE_KEY`. Keys starting with `G1T_` or `GITHUB_` are g1t's own. | |
| 30 | | **Value** | Up to 48 KB. | |
| 31 | | **Brokered** | A secret only: the hosts it is for. Jobs get a stand-in, and g1t fills in the value on their requests to those hosts. See [brokered secrets](#brokered-secrets). | |
| 32 | | **Note** | Optional: where to rotate it, or who to ask. | |
| 33 | | **Environments** | **All environments**, or only some: **Production**, **Preview**, or any name a workflow job uses in `environment:`, such as `staging`. | |
| 34 | | **Available to** | **Workflows**, **Deployments**, or both (the default). | |
| 35 | | **Projects** | A workspace's row only: every project, or the ones you choose. | |
| 36 | |
| 37 | ### A value per environment |
| 38 | |
| 39 | A key can have one row per environment, so production and previews use |
| 40 | different values. For example, Stripe's live key in production and its |
| 41 | test key everywhere else: |
| 42 | |
| 43 | | Key | Type | Environments | Value | |
| 44 | | --- | --- | --- | --- | |
| 45 | | `STRIPE_KEY` | Secret | Production | `sk_live_…` | |
| 46 | | `STRIPE_KEY` | Secret | Preview | `sk_test_…` | |
| 47 | |
| 48 | Two rows of the same key and type cannot apply to the same environment. |
| 49 | A key can also have a row for all environments alongside rows for some: |
| 50 | the rows for some win where they apply. |
| 51 | |
| 52 | ### Adding many at once |
| 53 | |
| 54 | Paste the contents of a `.env` file into **Key** when adding. Each |
| 55 | `KEY=value` line becomes a row with the type, environments and readers you |
| 56 | choose; blank lines and `#` comments are skipped. |
| 57 | |
| 58 | ## Who reads what |
| 59 | |
| 60 | | Reader | Reads | Environment it asks for | |
| 61 | | --- | --- | --- | |
| 62 | | A workflow job | Rows available to Workflows: secrets as `${{ secrets.KEY }}`, config as `${{ vars.KEY }}` | The job's `environment:`, if it has one | |
| 63 | | A deploy build | Rows available to Deployments, as environment variables | `production` or `preview` | |
| 64 | | A running app | The same rows, as bindings: `env.KEY` (a secret as a secret binding) | `production` or `preview` | |
| 65 | | The merge queue's builds | Nothing | | |
| 66 | |
| 67 | For each key, a reader gets the row for its environment if there is one, |
| 68 | else the row for all environments. A row only for other environments gives |
| 69 | it nothing. |
| 70 | |
| 71 | A project's row overrides its workspace's of the same key. The project's |
| 72 | list shows the workspace's rows that reach it, marked |
| 73 | **Workspace**, until it sets the key itself. |
| 74 | |
| 75 | A running app's rows are bound by g1t when it puts the app up; they never |
| 76 | pass through the build's sandbox. A row of a Workers project's own |
| 77 | `vars` with the same name is replaced. |
| 78 | |
| 79 | ## Who gets secrets |
| 80 | |
| 81 | Secrets go only to trusted runs: |
| 82 | |
| 83 | - pushes, schedules, manual runs and the merge queue; |
| 84 | - pull requests whose author has the Write [role](/guides/access-and-roles/) |
| 85 | or higher on the repository, a member or an |
| 86 | [outside collaborator](/guides/access-and-roles/#outside-collaborators); |
| 87 | - pull requests g1t opened on its own, such as a security update. |
| 88 | |
| 89 | Anyone else's pull request, such as one from a fork or by someone with |
| 90 | Read or Triage (who may open one on a private repository too), runs its |
| 91 | workflows, and builds its preview, with config only: no secrets, and a |
| 92 | `G1T_TOKEN` that can only read. Its runs may first wait for someone with |
| 93 | Write to [approve them](/guides/actions/#pull-requests-from-outside). |
| 94 | |
| 95 | A job that names an environment gets that environment's secrets only once |
| 96 | the environment's [protection rules](/guides/actions/#environments) let it |
| 97 | through: required reviewers, a wait timer, and which branches may deploy. |
| 98 | |
| 99 | ## Brokered secrets |
| 100 | |
| 101 | A secret a job uses only to call an API need not enter the job at all. |
| 102 | Broker it, and the job gets a stand-in in its place: a token such as |
| 103 | `g1tsec_3f9c…` (48 random characters after the prefix), made for that job |
| 104 | and that secret alone. As the job's requests leave its sandbox, g1t puts |
| 105 | the real value in place of the stand-in on HTTPS requests to the hosts |
| 106 | the secret is for; never over plain HTTP, where it would cross the |
| 107 | internet readable. Code in the job, a compromised dependency included, can |
| 108 | read only the stand-in, which works nowhere else and stops working when |
| 109 | the job ends. |
| 110 | |
| 111 | To broker a secret: |
| 112 | |
| 113 | 1. Open **Settings → Secrets and variables**, and add or edit the secret. |
| 114 | 2. Under **Brokered**, tick **Only for these hosts**. |
| 115 | 3. List the hosts it is for, one per line: `registry.npmjs.org`, or |
| 116 | `*.example.com` for every subdomain of `example.com` (not |
| 117 | `example.com` itself; list both if you need both). |
| 118 | 4. Save. |
| 119 | |
| 120 | Workflows use it as they would any secret: `${{ secrets.NPM_TOKEN }}` in an |
| 121 | `Authorization` header, an `.npmrc`, or a tool's settings. |
| 122 | |
| 123 | ```yaml |
| 124 | - run: npm publish |
| 125 | env: |
| 126 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
| 127 | ``` |
| 128 | |
| 129 | ### Where the value is filled in |
| 130 | |
| 131 | | In the request | | |
| 132 | | --- | --- | |
| 133 | | Any header | As it is: `Authorization: Bearer …`, `Authorization: token …`, `X-Api-Key: …` | |
| 134 | | `Authorization: Basic` | Decoded, filled in, and encoded again, so `user:$TOKEN` works | |
| 135 | | The query string | `?key=…`, encoded for the URL | |
| 136 | | The path and the body | Never: the stand-in goes as it is | |
| 137 | |
| 138 | A request to a host the secret is not for goes out with the stand-in, |
| 139 | which is useless to whoever receives it, and the job's log says so once: |
| 140 | `NPM_TOKEN was sent to example.com, which it is not for, so g1t did not |
| 141 | fill it in.` |
| 142 | |
| 143 | If an answer echoes the value back, it is swapped for the stand-in again |
| 144 | in the answer's headers, and in its body when that is plain text (JSON, |
| 145 | XML, text) of at most 256 KB sent uncompressed. A request with a filled-in |
| 146 | value is not followed through redirects outside the sandbox: the job |
| 147 | follows them itself, and each is filled in only for the secret's hosts. |
| 148 | |
| 149 | ### Where it works |
| 150 | |
| 151 | - **Workflow jobs on g1t's machines.** A job that may be given a |
| 152 | brokered secret sends its HTTP and HTTPS through g1t's runner Worker, |
| 153 | which fills the value in and sends every request on, to any host, and |
| 154 | the job trusts the certificate its HTTPS is re-signed with. Other jobs' |
| 155 | requests go straight out. Other protocols are not affected. |
| 156 | - **Self-hosted runners on Linux and macOS**, in Docker, in the desktop |
| 157 | app's sandbox, or directly on the computer: the job's requests to the |
| 158 | secret's hosts go through g1t, which fills the value in, so it never |
| 159 | reaches your computer. See |
| 160 | [on a self-hosted runner](#on-a-self-hosted-runner). |
| 161 | - **Self-hosted runners whose jobs run on Windows** (Windows Sandbox, or |
| 162 | directly on Windows): not yet. A job there gets the stand-in, never the |
| 163 | value, and its log warns that requests using it will not be signed in. |
| 164 | So does a job on a runner from before brokering. Keep a separate, |
| 165 | unbrokered secret for jobs that must run there. |
| 166 | - **Deployments** never read a brokered secret: a running app needs the |
| 167 | value itself. A brokered secret is available to workflows only. |
| 168 | - **Security updates' private registries** do not get a brokered secret. |
| 169 | |
| 170 | The value is masked in the job's log as any secret is, should it ever |
| 171 | appear there, and so is the stand-in. |
| 172 | |
| 173 | ### On a self-hosted runner |
| 174 | |
| 175 | A self-hosted runner's traffic does not leave through g1t, and g1t never |
| 176 | sends a brokered value to a computer it does not run. So the runner |
| 177 | brings the requests that need the value to g1t instead: |
| 178 | |
| 179 | 1. The job gets the stand-in, as on g1t's machines, and the runner is |
| 180 | told which hosts each stand-in is for. It is never told a value. |
| 181 | 2. The job's steps get `HTTPS_PROXY` pointing at a proxy the runner runs |
| 182 | for that job on `127.0.0.1`. HTTPS to any host the job's brokered |
| 183 | secrets are not for passes through it as a plain tunnel, which the |
| 184 | runner never sees inside. |
| 185 | 3. HTTPS to one of those hosts is opened with a certificate the runner |
| 186 | makes for that host. It comes from a certificate authority made for |
| 187 | that job alone, which can sign for the brokered hosts and no others, |
| 188 | and whose key is never written to disk. The steps trust it through the |
| 189 | same variables as on g1t's machines (`SSL_CERT_FILE`, |
| 190 | `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, |
| 191 | `PIP_CERT`, `GIT_SSL_CAINFO`, `CARGO_HTTP_CAINFO`), each pointing at |
| 192 | what it pointed at before with the job's certificate added. |
| 193 | 4. A request on that connection that carries one of the job's stand-ins |
| 194 | goes to g1t with the job's token. g1t fills the value in as described |
| 195 | above, sends the request without following redirects, and sends back |
| 196 | the answer, with the value swapped back for its stand-in in its headers |
| 197 | and in any text body, whatever its size. A request without a stand-in |
| 198 | goes straight to the host. |
| 199 | |
| 200 | g1t relays only a request that carries one of the job's own stand-ins, |
| 201 | over HTTPS, to one of that secret's hosts, and only while the job runs, |
| 202 | with the token the job holds now. Anything else is refused, and the job's |
| 203 | log says so once. The log's first lines name each brokered secret and the |
| 204 | hosts its requests go to g1t for. |
| 205 | |
| 206 | Requests to brokered hosts take the longer way, through g1t, so they are |
| 207 | a little slower, and have limits: |
| 208 | |
| 209 | | Limit | | |
| 210 | | --- | --- | |
| 211 | | A request's body | 32 MB | |
| 212 | | An answer's body | 1 GB | |
| 213 | | One request, its answer included | 10 minutes | |
| 214 | | HTTP versions | HTTP/1.1 between the job and the runner; plain HTTP is never filled in, and `HTTP_PROXY` is left as your computer has it | |
| 215 | |
| 216 | What a self-hosted runner does not cover: |
| 217 | |
| 218 | - **Jobs that run on Windows**, as above. |
| 219 | - **Containers a step starts**: a Docker action, `services:`, or |
| 220 | `docker run` in a step. They do not get the job's proxy, so their |
| 221 | requests carry the stand-in as it is. |
| 222 | - **A computer that sets its own `HTTPS_PROXY`.** The runner does not |
| 223 | chain proxies: its jobs get the stand-in and the warning. |
| 224 | - **Tools that ignore `HTTPS_PROXY`** or bring their own trusted |
| 225 | certificates send the stand-in as it is. npm and most other package |
| 226 | managers, `curl`, `git`, Cargo, Python's `requests` and Go use it. |
| 227 | Node.js's own `fetch` and `https` use it in the releases that read |
| 228 | `NODE_USE_ENV_PROXY`, which the runner sets. |
| 229 | |
| 230 | ### What brokering cannot cover |
| 231 | |
| 232 | A secret the job itself must read cannot be brokered. Keep these as |
| 233 | ordinary secrets: |
| 234 | |
| 235 | - passwords for a database or service reached over raw TCP rather than |
| 236 | HTTP, such as PostgreSQL or Redis; |
| 237 | - signing keys (code signing, GPG, SSH, a release's private key), which |
| 238 | the job uses to sign rather than sends; |
| 239 | - keys to files the job decrypts; |
| 240 | - tests that need the real value inside the job; |
| 241 | - a value sent in a request's body or path, such as an OAuth client |
| 242 | secret posted in a form. |
| 243 | |
| 244 | A brokered value is at least 8 characters, so it is not found inside |
| 245 | other secrets by chance. |
| 246 | |
| 247 | ## G1T_TOKEN |
| 248 | |
| 249 | Every workflow job gets `${{ secrets.G1T_TOKEN }}`: a token for that job |
| 250 | alone, which reaches its repository only, can do what the job's |
| 251 | `permissions:` say, and stops working when the job ends. |
| 252 | `${{ secrets.GITHUB_TOKEN }}` and `${{ github.token }}` are the same token, |
| 253 | so workflows written for GitHub work unchanged. See |
| 254 | [the job's token](/guides/actions/#the-jobs-token). |
| 255 | |
| 256 | ```yaml |
| 257 | # On the job: permissions: { issues: write } |
| 258 | - name: Open an issue when the nightly build fails |
| 259 | if: failure() |
| 260 | run: | |
| 261 | curl -X POST https://api.g1t.sh/repos/${{ github.repository }}/issues \ |
| 262 | -H "Authorization: Bearer ${{ secrets.G1T_TOKEN }}" \ |
| 263 | -d '{"title":"Nightly build failed"}' |
| 264 | ``` |
| 265 | |
| 266 | `G1T_TOKEN` can neither list nor change secrets or variables, so a |
| 267 | workflow cannot read or rewrite what it runs with. No workspace access |
| 268 | token can change them either: use a person's token, or the site. |
| 269 | |
| 270 | ## From the API |
| 271 | |
| 272 | The routes are GitHub's, and calls written for GitHub work unchanged: a |
| 273 | key named without an `id` or `environments` is the key's row for all |
| 274 | environments. |
| 275 | |
| 276 | | MCP (`secret` tool) | Route | |
| 277 | | --- | --- | |
| 278 | | `list_secrets` | `GET /repos/{owner}/{repo}/actions/secrets` | |
| 279 | | `set_secret` | `PUT /repos/{owner}/{repo}/actions/secrets/{key}` | |
| 280 | | `delete_secret` | `DELETE /repos/{owner}/{repo}/actions/secrets/{key}` | |
| 281 | | `list_variables` | `GET /repos/{owner}/{repo}/actions/variables` | |
| 282 | | `set_variable` | `POST /repos/{owner}/{repo}/actions/variables`, `PATCH …/variables/{key}` | |
| 283 | | `delete_variable` | `DELETE /repos/{owner}/{repo}/actions/variables/{key}` | |
| 284 | |
| 285 | A workspace's are under `/workspaces/{workspace}/actions/secrets` and |
| 286 | `…/variables`. Beyond GitHub's fields, a row takes: |
| 287 | |
| 288 | | Field | | |
| 289 | | --- | --- | |
| 290 | | `id` | The row to change or remove, from a list. | |
| 291 | | `available_to` | `["workflows"]`, `["deployments"]` or both. `availableTo` works too. | |
| 292 | | `environments` | `["production"]`, `["preview", "staging"]`; `[]` for all. | |
| 293 | | `projects` | A workspace's row: project names; `[]` for every one. (`repositories` is read the same way.) | |
| 294 | | `note` | Where to rotate it, or who to ask. | |
| 295 | | `brokered_hosts` | A secret's: `["registry.npmjs.org"]`, `["*.example.com"]` to [broker it](#brokered-secrets); `[]` stops brokering it. Listed rows return it too. | |
| 296 | |
| 297 | ```sh |
| 298 | curl -X PUT https://api.g1t.sh/repos/acme/web/actions/secrets/STRIPE_KEY \ |
| 299 | -H "Authorization: Bearer $YOUR_TOKEN" \ |
| 300 | -d '{"value":"sk_live_…","environments":["production"],"available_to":["deployments"]}' |
| 301 | ``` |
| 302 | |
| 303 | A secret is sent as plain `value` over HTTPS, not encrypted to a public |
| 304 | key first. |