Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

304 linesCodeBlameRaw
1---
2title: Secrets and variables
3description: One list of keys and values for workflows and deployments. Each row says which environments it applies to and who reads it.
4---
5
6A [project](/guides/projects/) and a workspace each have one list of
7secrets and variables.
8Workflows and deployments both read from it; each row says whether one,
9the 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
16Seeing the list needs the same as changing it: a project's rows, config
17values included, are for Admins of its repository, and the page is not
18shown to anyone else.
19
20A workflow that deploys to AWS, Google Cloud or Azure needs no cloud key
21here: 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
39A key can have one row per environment, so production and previews use
40different values. For example, Stripe's live key in production and its
41test 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
48Two rows of the same key and type cannot apply to the same environment.
49A key can also have a row for all environments alongside rows for some:
50the rows for some win where they apply.
51
52### Adding many at once
53
54Paste 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
56choose; 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
67For each key, a reader gets the row for its environment if there is one,
68else the row for all environments. A row only for other environments gives
69it nothing.
70
71A project's row overrides its workspace's of the same key. The project's
72list shows the workspace's rows that reach it, marked
73**Workspace**, until it sets the key itself.
74
75A running app's rows are bound by g1t when it puts the app up; they never
76pass 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
81Secrets 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
89Anyone else's pull request, such as one from a fork or by someone with
90Read or Triage (who may open one on a private repository too), runs its
91workflows, 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
93Write to [approve them](/guides/actions/#pull-requests-from-outside).
94
95A job that names an environment gets that environment's secrets only once
96the environment's [protection rules](/guides/actions/#environments) let it
97through: required reviewers, a wait timer, and which branches may deploy.
98
99## Brokered secrets
100
101A secret a job uses only to call an API need not enter the job at all.
102Broker 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
104and that secret alone. As the job's requests leave its sandbox, g1t puts
105the real value in place of the stand-in on HTTPS requests to the hosts
106the secret is for; never over plain HTTP, where it would cross the
107internet readable. Code in the job, a compromised dependency included, can
108read only the stand-in, which works nowhere else and stops working when
109the job ends.
110
111To broker a secret:
112
1131. Open **Settings → Secrets and variables**, and add or edit the secret.
1142. Under **Brokered**, tick **Only for these hosts**.
1153. 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).
1184. Save.
119
120Workflows 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
138A request to a host the secret is not for goes out with the stand-in,
139which 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
141fill it in.`
142
143If an answer echoes the value back, it is swapped for the stand-in again
144in the answer's headers, and in its body when that is plain text (JSON,
145XML, text) of at most 256 KB sent uncompressed. A request with a filled-in
146value is not followed through redirects outside the sandbox: the job
147follows 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
170The value is masked in the job's log as any secret is, should it ever
171appear there, and so is the stand-in.
172
173### On a self-hosted runner
174
175A self-hosted runner's traffic does not leave through g1t, and g1t never
176sends a brokered value to a computer it does not run. So the runner
177brings the requests that need the value to g1t instead:
178
1791. 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.
1812. 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.
1853. 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.
1934. 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
200g1t relays only a request that carries one of the job's own stand-ins,
201over HTTPS, to one of that secret's hosts, and only while the job runs,
202with the token the job holds now. Anything else is refused, and the job's
203log says so once. The log's first lines name each brokered secret and the
204hosts its requests go to g1t for.
205
206Requests to brokered hosts take the longer way, through g1t, so they are
207a 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
216What 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
232A secret the job itself must read cannot be brokered. Keep these as
233ordinary 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
244A brokered value is at least 8 characters, so it is not found inside
245other secrets by chance.
246
247## G1T_TOKEN
248
249Every workflow job gets `${{ secrets.G1T_TOKEN }}`: a token for that job
250alone, 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,
253so 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
267workflow cannot read or rewrite what it runs with. No workspace access
268token can change them either: use a person's token, or the site.
269
270## From the API
271
272The routes are GitHub's, and calls written for GitHub work unchanged: a
273key named without an `id` or `environments` is the key's row for all
274environments.
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
285A 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
298curl -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
303A secret is sent as plain `value` over HTTPS, not encrypted to a public
304key first.