onesecret 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +313 -2
- package/bin/help.txt +37 -0
- package/bin/index.js +14 -0
- package/package.json +106 -3
- package/src/age.js +363 -0
- package/src/bech32.js +89 -0
- package/src/cli.js +347 -0
- package/src/deploy.js +113 -0
- package/src/dotenv.js +67 -0
- package/src/error.js +12 -0
- package/src/fly.js +109 -0
- package/src/format.js +61 -0
- package/src/index.js +20 -0
- package/src/keys.js +197 -0
- package/src/regexp.js +91 -0
- package/src/secrets.js +331 -0
- package/src/sops.js +434 -0
- package/src/store.js +257 -0
- package/src/write.js +288 -0
- package/src/yaml.js +361 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Jose Francisco 'Kiko' Verdú Gambín <josefrancisco.verdu@gmail.com> (kikobeats.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,314 @@
|
|
|
1
|
-
#
|
|
1
|
+
# onesecret
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
[](https://coveralls.io/github/kikobeats/onesecret)
|
|
5
|
+
[](https://www.npmjs.org/package/onesecret)
|
|
6
|
+
|
|
7
|
+
> Keep just one secret to decrypt the rest.
|
|
8
|
+
|
|
9
|
+
Commit your credentials encrypted next to your code, and keep a single age key to read them. The files are [SOPS](https://github.com/getsops/sops) files, so the `sops` CLI and every tool around it keep working, but nothing has to be installed: encryption and decryption run in Node.js with no native binaries.
|
|
10
|
+
|
|
11
|
+
- **One key.** One age key decrypts every environment. It lives in CI and in your team's password manager, never in the image and never in the running app.
|
|
12
|
+
- **Layers.** `config/default.yml` and `config/default.enc.yml` hold what every environment shares, and `config/production.yml` and `config/production.enc.yml` override them for production. Plain settings and secrets live side by side, and `config()` returns them as one object.
|
|
13
|
+
- **Production never needs the key.** Deploy pushes the decrypted values to the platform (a Kubernetes Secret, Fly secrets). At boot, `config()` sees every value already in the environment and decrypts nothing. In `production` and `staging` it never runs `keyCommand`, so a missing secret fails the boot instead of fetching a key.
|
|
14
|
+
- **Fails fast.** Secret names are stored in plaintext, so `config()` knows what must exist and names exactly what is missing.
|
|
15
|
+
- **Fast.** After about 9ms to load the module, the first `config()` call takes about 5ms when it decrypts two layers, against roughly 30ms for spawning `sops -d` twice, and about 4ms when the platform already injected every secret and nothing is decrypted. Later calls take about 0.3ms.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
$ npm install onesecret
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
$ npx onesecret init
|
|
27
|
+
public key age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
|
28
|
+
private key AGE-SECRET-KEY-1…
|
|
29
|
+
secrets config/default.enc.yml
|
|
30
|
+
|
|
31
|
+
The private key is shown only now and is not saved anywhere. Keep it in your
|
|
32
|
+
password manager and as SOPS_AGE_KEY in CI. To use it in this shell:
|
|
33
|
+
|
|
34
|
+
export SOPS_AGE_KEY=AGE-SECRET-KEY-1…
|
|
35
|
+
|
|
36
|
+
$ export SOPS_AGE_KEY=AGE-SECRET-KEY-1…
|
|
37
|
+
$ npx onesecret set DATABASE_URL=postgres://localhost/app STRIPE_KEY=sk_test_123
|
|
38
|
+
$ npx onesecret set --env production DATABASE_URL=postgres://db.internal/app
|
|
39
|
+
$ git add config
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`init` prints the private key once and writes it nowhere, so it never sits in a file on your disk. Save it in your password manager before closing the terminal: without it, nobody can decrypt the files.
|
|
43
|
+
|
|
44
|
+
There is no other configuration. Each encrypted file records the public key it is locked for, so a new layer such as `config/staging.enc.yml` is locked for the same key as `config/default.enc.yml` in its folder, with the same encryption rules. A file whose folder has no `default.enc.yml` is locked for the public key of the `SOPS_AGE_KEY` you exported. To join an existing project, export its key first: `init` then uses it and prints no new key.
|
|
45
|
+
|
|
46
|
+
Load them as the first thing your app does:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const config = require('onesecret').config()
|
|
50
|
+
|
|
51
|
+
console.log(config.DATABASE_URL)
|
|
52
|
+
console.log(process.env.DATABASE_URL)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`config()` reads the layer named by `NODE_ENV`. To pick another one, pass its name: `config('staging')`.
|
|
56
|
+
|
|
57
|
+
To change a secret, open its file. You edit it decrypted, and it is encrypted again when you save:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ npx onesecret edit config/production.enc.yml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or run anything with them, without touching its code:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$ npx onesecret exec -- node server.js
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
[`example/`](example) is a runnable app that shows each of these cases.
|
|
70
|
+
|
|
71
|
+
## How it works
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
config/default.yml ← plain settings shared by every environment
|
|
75
|
+
config/default.enc.yml ← secrets shared by every environment
|
|
76
|
+
config/production.yml ← plain settings for NODE_ENV=production
|
|
77
|
+
config/production.enc.yml ← secrets for NODE_ENV=production
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Every file is optional. They are merged in that order, so a later file wins, and nested objects are merged key by key. Plain files are YAML (`.yml`, `.yaml`) or JSON (`.json`), read with [js-yaml](https://github.com/nodeca/js-yaml); an empty file is an empty layer. Each layer has at most one plain and one encrypted file, so `default.yml` next to `default.json` is an error, and so is a sops-encrypted file named like a plain one.
|
|
81
|
+
|
|
82
|
+
A name is a secret while an encrypted file contributes to its merged value. A plain file that replaces a secret from a lower layer makes the name plain config for that env; a plain mapping merged into an encrypted one keeps it a secret. `config()` reads the secret names (they are plaintext in the encrypted files) and checks `process.env`:
|
|
83
|
+
|
|
84
|
+
1. Every secret is already set: nothing is decrypted, and the key is not needed. This is production. A variable set to an empty string counts as set.
|
|
85
|
+
2. Some secrets are missing: it finds the age key, decrypts, and fills **only** the missing ones.
|
|
86
|
+
3. Some secrets are missing and there is no key: it throws, naming each missing secret.
|
|
87
|
+
|
|
88
|
+
Values already in the environment always win, for plain settings too. Every other top-level value that is a string, number, boolean, or `null` is written into `process.env` as a string. `config()` returns the merged object with the YAML types kept, so `TIMEOUT: 60000` comes back as the number `60000`, while `process.env.TIMEOUT` is `"60000"`. Nested objects and lists, and names that are not valid variable names (such as `log-level`), are returned but not written into `process.env`. A value the environment provides comes back with its file's type when the string matches it, so `PORT=3000` is the number `3000` again, even for a secret, whose type is plaintext in the encrypted file. A secret must be a string, number, or boolean, since the platform injects it as a variable: a mapping or list secret missing from the environment fails with `EFORMAT`.
|
|
89
|
+
|
|
90
|
+
Encrypted files accept only the standard tags `!!str`, `!!int`, `!!float`, `!!bool`, `!!null`, and `!!timestamp`, on values, so a value such as `!Passw0rd` must be quoted. A YAML error names the line and column but never the text, so a broken line cannot print a secret. Env names are lowercase letters, digits, `.`, `_`, and `-`, without `..`, an `enc` part, or a file extension, so `NODE_ENV=Production` fails at boot instead of reading another file. Layer files must match that exact name: `Production.yml` next to a `production` layer is refused, so a layer reads the same on every filesystem. It also throws when it finds no file at all, so an image that forgot to copy `config/`, or a process started from the wrong directory, fails at boot instead of running without its configuration.
|
|
91
|
+
|
|
92
|
+
The age key is read from these sources, and every key found is tried:
|
|
93
|
+
|
|
94
|
+
- `SOPS_AGE_KEY`, the key itself (the usual choice in CI).
|
|
95
|
+
- `SOPS_AGE_KEY_FILE`, a path to a keys file.
|
|
96
|
+
- The sops keys file: `$XDG_CONFIG_HOME/sops/age/keys.txt`, `~/Library/Application Support/sops/age/keys.txt` on macOS, and `~/.config/sops/age/keys.txt` on any system.
|
|
97
|
+
|
|
98
|
+
A source can hold several keys, one per line or separated by spaces. Post-quantum (`AGE-SECRET-KEY-PQ-`) and plugin identities are skipped, and SSH keys are not read. A source that cannot be read or parsed, such as a keys file holding an SSH key, is skipped when another source has a key; when no key opens the file, the error names the sources it skipped.
|
|
99
|
+
|
|
100
|
+
When none of them opens the file, `SOPS_AGE_KEY_CMD` or the `keyCommand` setting runs: a command that prints the key. It runs at most once per process, whether it succeeds or fails, and `config()` never runs it in `production` or `staging`. `load()` and the CLI (`exec`, `get`, `dotenv`, deploys) do run it in any env, since you call them on purpose.
|
|
101
|
+
|
|
102
|
+
`keyCommand` comes from the project's `package.json`, so it has the same trust model as an npm script: only run onesecret in repositories you trust.
|
|
103
|
+
|
|
104
|
+
With `keyCommand`, nobody keeps the key on disk. For example, to read it from a GitLab CI variable (`glab`) or from 1Password (`op`):
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"onesecret": {
|
|
109
|
+
"keyCommand": "glab variable get SOPS_AGE_KEY -R my-group/my-project"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`exec`, `kubernetes`, and `fly` remove `SOPS_AGE_KEY`, `SOPS_AGE_KEY_FILE`, and `SOPS_AGE_KEY_CMD` from the environment they pass on, so the command you run gets the secrets but not the key. The keys file stays readable on disk to anything running as you, and `edit` and `keyCommand` run with your full environment.
|
|
115
|
+
|
|
116
|
+
## Deploy
|
|
117
|
+
|
|
118
|
+
### Kubernetes
|
|
119
|
+
|
|
120
|
+
Deploys push only the secrets, with their merged values. Plain settings ship inside the image with the `config/` directory.
|
|
121
|
+
|
|
122
|
+
Write the secrets into a Secret before `helm upgrade`, then pass the printed checksum to the chart so pods roll when a value changes:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
$ checksum=$(npx onesecret kubernetes --env production --name api-env --namespace api --context prod)
|
|
126
|
+
$ helm upgrade api ./chart --set-string envSecretChecksum=$checksum
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# chart/templates/deployment.yaml
|
|
131
|
+
spec:
|
|
132
|
+
template:
|
|
133
|
+
metadata:
|
|
134
|
+
annotations:
|
|
135
|
+
checksum/api-env: {{ .Values.envSecretChecksum | quote }}
|
|
136
|
+
spec:
|
|
137
|
+
containers:
|
|
138
|
+
- name: api
|
|
139
|
+
envFrom:
|
|
140
|
+
- secretRef:
|
|
141
|
+
name: api-env
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Apply the Secret before every install, including one-off installs such as a canary, because a pod that reads a Secret that does not exist yet never becomes ready. The checksum annotation matters too: when only a secret value changes, nothing else in the pod template changes, so without it Kubernetes keeps the old pods running with the old values.
|
|
145
|
+
|
|
146
|
+
### Fly.io
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
$ npx onesecret fly --env production --app my-app
|
|
150
|
+
$ fly deploy --app my-app
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`kubernetes` and `fly` require `--env`, and that layer must have a plain or an encrypted file, so a typo such as `--env prodution` or a wrong `--dir` fails instead of pushing the wrong secrets. To deploy the default layer alone, pass `--env default`.
|
|
154
|
+
|
|
155
|
+
Secrets are staged and applied by the next deploy. Pass `--no-stage` to restart the machines with them right away. Every value is encoded so that `fly secrets import` reads it back unchanged, including `#`, quotes, and multi-line values such as PEM keys. onesecret checks this against a port of flyctl's own parser before sending anything, and refuses the rare value that cannot round-trip (for example a line over 64 KiB, which flyctl drops). Keep secret names out of the `[env]` section of `fly.toml`, since those values would be stored as plain configuration. `fly secrets import` adds and updates but never removes, so when a secret moves to a plain file, run `fly secrets unset NAME`, or the old secret keeps overriding the file.
|
|
156
|
+
|
|
157
|
+
### Anywhere else
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
$ npx onesecret dotenv --env production > .env.production
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Values are quoted only when needed, and every line is checked against both Node.js's own `.env` parser and the `dotenv` package before it is printed, so `#`, spaces, quotes, backslashes, and newlines read back unchanged with `node --env-file` or `dotenv`. A value that no quoting can carry through both, such as one starting with `'` and ending with `\`, is refused instead of corrupted.
|
|
164
|
+
|
|
165
|
+
Keep the `config/` directory in your image: `config()` reads the plain settings from it and the secret names to check that the platform injected every secret, and fails at boot if one is missing.
|
|
166
|
+
|
|
167
|
+
## Tests
|
|
168
|
+
|
|
169
|
+
Unit tests usually do not need real credentials. Set `ONESECRET_SKIP=1` and `config()` never decrypts: it still loads the plain settings, and leaves unset any secret the environment does not provide.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
$ ONESECRET_SKIP=1 npm test
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Integration tests that need credentials can run under `onesecret exec -- ava`, which decrypts once and hands the values, never the key, to every test worker.
|
|
176
|
+
|
|
177
|
+
## CLI
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
$ onesecret --help
|
|
181
|
+
|
|
182
|
+
Usage
|
|
183
|
+
$ onesecret <command> [options]
|
|
184
|
+
|
|
185
|
+
Commands
|
|
186
|
+
init Write config/default.enc.yml for a new age key it prints once,
|
|
187
|
+
or for SOPS_AGE_KEY when exported
|
|
188
|
+
set NAME=value... Encrypt one or more secrets into a layer
|
|
189
|
+
unset NAME... Remove secrets from a layer
|
|
190
|
+
edit [file] Open an encrypted file decrypted in $EDITOR, encrypt it on save
|
|
191
|
+
(a plain .yml or .json file opens as it is)
|
|
192
|
+
rekey Encrypt a layer again with a new data key, for its recipients or a .sops.yaml rule
|
|
193
|
+
ls List secret names (no key needed)
|
|
194
|
+
get NAME Print one value, from a plain or an encrypted layer
|
|
195
|
+
dotenv Print every decrypted secret as NAME=value
|
|
196
|
+
exec -- <command> Run a command with the config and secrets in its environment
|
|
197
|
+
(--env also sets its NODE_ENV)
|
|
198
|
+
kubernetes --name <name> Apply the secrets as a Kubernetes Secret, print its checksum
|
|
199
|
+
fly --app <app> Import the secrets into a Fly app, print their checksum
|
|
200
|
+
|
|
201
|
+
Options
|
|
202
|
+
--env, -e Layer to read on top of "default" (defaults to NODE_ENV, then "development").
|
|
203
|
+
Write commands (set, unset, edit, rekey) write this layer, "default" when omitted.
|
|
204
|
+
kubernetes and fly require it, and the layer file must exist ("default" deploys the base alone).
|
|
205
|
+
--dir Directory holding the config files (defaults to "config")
|
|
206
|
+
--cwd Project root, where package.json and the config directory are (defaults to the current directory)
|
|
207
|
+
--namespace, -n Kubernetes namespace
|
|
208
|
+
--context kubectl context
|
|
209
|
+
--no-stage fly: apply the secrets now instead of on the next deploy
|
|
210
|
+
|
|
211
|
+
Examples
|
|
212
|
+
$ onesecret init
|
|
213
|
+
$ onesecret set DATABASE_URL=postgres://localhost/app
|
|
214
|
+
$ onesecret set --env production DATABASE_URL=postgres://db.internal/app
|
|
215
|
+
$ onesecret edit config/production.enc.yml
|
|
216
|
+
$ onesecret exec -- node server.js
|
|
217
|
+
$ onesecret kubernetes --env production --name api-env --namespace api
|
|
218
|
+
$ onesecret fly --env production --app my-app
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`set`, and `edit` of an encrypted file, only accept names that are valid environment variables, refuse `sops` (reserved for the file metadata), and refuse to write a value that the file's rules would leave in plaintext. A name ending in `_unencrypted` is stored in plaintext on purpose, as sops does.
|
|
222
|
+
|
|
223
|
+
When an edit is not valid YAML, the editor opens again with your changes, and the error never prints the decrypted content.
|
|
224
|
+
|
|
225
|
+
`edit` opens `$VISUAL` or `$EDITOR` (then `vi`). GUI editors that return at once, such as VS Code, Cursor, Zed, or Sublime Text, get their wait flag added, so `EDITOR=code` works without `--wait`. Any other editor command must wait until the file is closed, as git requires too. It takes the path of the file to open, such as `config/production.enc.yml`, or edits the `--env` layer when no path is given. A path that does not exist yet is locked for the recipients of `default.enc.yml` in its folder, or else for your exported `SOPS_AGE_KEY`, and is refused before the editor opens when there is neither. A plain `.yml` or `.json` path opens as it is; when it is saved invalid, the editor opens again. `get` reads a plain value without the key, and prints a nested value as YAML.
|
|
226
|
+
|
|
227
|
+
### Separate keys per person
|
|
228
|
+
|
|
229
|
+
One shared key needs no configuration. To give each person or CI its own key instead, list their public keys in a `.sops.yaml` rule, the format the `sops` CLI uses, then encrypt each layer again:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
# .sops.yaml
|
|
233
|
+
creation_rules:
|
|
234
|
+
- path_regex: ^config/
|
|
235
|
+
age: age1…alice,age1…bob,age1…ci
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
$ npx onesecret rekey
|
|
240
|
+
$ npx onesecret rekey --env production
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When a `.sops.yaml` rule matches a file, its recipients win over the ones the file already lists. Removing someone stops them from reading future versions, but they can still read every version they already had. Rotate the credentials themselves too.
|
|
244
|
+
|
|
245
|
+
## API
|
|
246
|
+
|
|
247
|
+
`config`, `load`, and `names` take the env name as a string, or these options, which `kubernetes` and `fly` take too:
|
|
248
|
+
|
|
249
|
+
| Option | Default | Description |
|
|
250
|
+
| ------------ | ---------------------------------- | ----------------------------------------------------- |
|
|
251
|
+
| `cwd` | `process.cwd()` | Project root, where `package.json` is read from. |
|
|
252
|
+
| `dir` | `"config"` | Directory with the config files, relative to `cwd`. |
|
|
253
|
+
| `env` | `NODE_ENV`, then `"development"` | Layer merged on top of `default`. |
|
|
254
|
+
| `keyCommand` | none | Command that prints the age key. |
|
|
255
|
+
| `processEnv` | `process.env` | `config()` only: the environment to read and fill, including `NODE_ENV` and `ONESECRET_SKIP`. |
|
|
256
|
+
|
|
257
|
+
`dir` and `keyCommand` can also be set in `package.json` under `"onesecret"`.
|
|
258
|
+
|
|
259
|
+
### config([env | options])
|
|
260
|
+
|
|
261
|
+
Merges the layers, fills the missing values into `process.env` (or `options.processEnv`), and returns the merged object, with the values the environment already had taking their place. Throws an `OnesecretError` with code `EMISSING` when a secret is missing and there is no key to decrypt it.
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
const config = require('onesecret').config() // NODE_ENV, then "development"
|
|
265
|
+
const staging = require('onesecret').config('staging')
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### load([env | options])
|
|
269
|
+
|
|
270
|
+
Returns every layer merged and decrypted, ignoring `process.env`. Values keep their YAML type.
|
|
271
|
+
|
|
272
|
+
### names([env | options])
|
|
273
|
+
|
|
274
|
+
Returns the secret names, without needing the key.
|
|
275
|
+
|
|
276
|
+
### kubernetes({ name, namespace, context, args, kubectl, ...options })
|
|
277
|
+
|
|
278
|
+
Applies the secrets as an `Opaque` Secret with `kubectl apply`. Returns `{ checksum, output }`.
|
|
279
|
+
|
|
280
|
+
### fly({ app, stage = true, flyctl, ...options })
|
|
281
|
+
|
|
282
|
+
Imports the secrets with `fly secrets import`. Returns `{ checksum, output }`.
|
|
283
|
+
|
|
284
|
+
### toEnv(secrets), toDotenv(secrets), checksum(secrets), withoutAgeKey(env)
|
|
285
|
+
|
|
286
|
+
Helpers used by the commands above: turn secrets into environment strings, render them as dotenv, hash them in an order-independent way, and drop the age key variables from an environment.
|
|
287
|
+
|
|
288
|
+
### Errors
|
|
289
|
+
|
|
290
|
+
Every error is an `OnesecretError` with a `code`:
|
|
291
|
+
|
|
292
|
+
| Code | Meaning |
|
|
293
|
+
| ----------- | ------------------------------------------------------------------ |
|
|
294
|
+
| `EMISSING` | A secret is not in the environment and could not be decrypted. |
|
|
295
|
+
| `ENOKEY` | No age key was found. |
|
|
296
|
+
| `EKEY` | A key was found but does not open the file, or `keyCommand` failed. |
|
|
297
|
+
| `EMAC` | The file was modified without the key. |
|
|
298
|
+
| `EFORMAT` | A file cannot be read: not valid YAML or JSON, not a supported SOPS file, two files for one layer, or a secret `config()` could never find in the environment. |
|
|
299
|
+
| `EVALUE` | A value cannot be stored or turned into an environment variable. |
|
|
300
|
+
| `EDEPLOY` | `kubectl` or `fly` failed. |
|
|
301
|
+
| `EUSAGE` | A CLI command was called the wrong way, or the editor failed. |
|
|
302
|
+
|
|
303
|
+
## Compatibility
|
|
304
|
+
|
|
305
|
+
onesecret reads and writes SOPS files encrypted for age X25519 recipients, in YAML or JSON, and reads every value type sops writes in those formats (`str`, `int`, `float`, `bool`, `time`). Values in encrypted files resolve the way go-yaml v3 does in sops, so `0777` is 511 and `2024-01-01` is a timestamp. Plain files are read with js-yaml instead, where `0777` is 777, YAML merge keys (`<<`) are not applied, and integers beyond 2^53 lose precision, so moving a value between a plain and an encrypted file can change it. Integers beyond 2^53 come back as `BigInt`, and timestamps as RFC 3339 strings, which are written back as strings. Rules and `path_regex` accept Go regular expression syntax such as `(?i)`, `\A`, `\z`, and `[[:lower:]]`. The test suite checks all of this against `sops` 3.13 and `age` 1.3 in both directions, including `mac_only_encrypted` and the `*_suffix` and `*_regex` rules. Like sops, it rejects truncated authentication tags and low-order age recipients.
|
|
306
|
+
|
|
307
|
+
It does not support PGP, cloud KMS, key groups, Shamir secret sharing, comment-based rules, or YAML anchors and multiple documents in an encrypted file (`.sops.yaml` can use anchors). It refuses to write a file that also has KMS or PGP keys rather than drop them, and YAML comments are lost when it writes a file. Use the `sops` CLI for any of those.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
**onesecret** © [Jose Francisco 'Kiko' Verdú Gambín](https://kikobeats.com), released under the [MIT](https://github.com/kikobeats/onesecret/blob/master/LICENSE.md) License.<br>
|
|
312
|
+
Authored and maintained by [Jose Francisco 'Kiko' Verdú Gambín](https://kikobeats.com) with help from [contributors](https://github.com/kikobeats/onesecret/contributors).
|
|
313
|
+
|
|
314
|
+
> [kikobeats.com](https://kikobeats.com) · GitHub [Jose Francisco 'Kiko' Verdú Gambín](https://github.com/kikobeats) · Twitter [@kikobeats](https://twitter.com/kikobeats)
|
package/bin/help.txt
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Usage
|
|
2
|
+
$ onesecret <command> [options]
|
|
3
|
+
|
|
4
|
+
Commands
|
|
5
|
+
init Write config/default.enc.yml for a new age key it prints once,
|
|
6
|
+
or for SOPS_AGE_KEY when exported
|
|
7
|
+
set NAME=value... Encrypt one or more secrets into a layer
|
|
8
|
+
unset NAME... Remove secrets from a layer
|
|
9
|
+
edit [file] Open an encrypted file decrypted in $EDITOR, encrypt it on save
|
|
10
|
+
(a plain .yml or .json file opens as it is)
|
|
11
|
+
rekey Encrypt a layer again with a new data key, for its recipients or a .sops.yaml rule
|
|
12
|
+
ls List secret names (no key needed)
|
|
13
|
+
get NAME Print one value, from a plain or an encrypted layer
|
|
14
|
+
dotenv Print every decrypted secret as NAME=value
|
|
15
|
+
exec -- <command> Run a command with the config and secrets in its environment
|
|
16
|
+
(--env also sets its NODE_ENV)
|
|
17
|
+
kubernetes --name <name> Apply the secrets as a Kubernetes Secret, print its checksum
|
|
18
|
+
fly --app <app> Import the secrets into a Fly app, print their checksum
|
|
19
|
+
|
|
20
|
+
Options
|
|
21
|
+
--env, -e Layer to read on top of "default" (defaults to NODE_ENV, then "development").
|
|
22
|
+
Write commands (set, unset, edit, rekey) write this layer, "default" when omitted.
|
|
23
|
+
kubernetes and fly require it, and the layer file must exist ("default" deploys the base alone).
|
|
24
|
+
--dir Directory holding the config files (defaults to "config")
|
|
25
|
+
--cwd Project root, where package.json and the config directory are (defaults to the current directory)
|
|
26
|
+
--namespace, -n Kubernetes namespace
|
|
27
|
+
--context kubectl context
|
|
28
|
+
--no-stage fly: apply the secrets now instead of on the next deploy
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
$ onesecret init
|
|
32
|
+
$ onesecret set DATABASE_URL=postgres://localhost/app
|
|
33
|
+
$ onesecret set --env production DATABASE_URL=postgres://db.internal/app
|
|
34
|
+
$ onesecret edit config/production.enc.yml
|
|
35
|
+
$ onesecret exec -- node server.js
|
|
36
|
+
$ onesecret kubernetes --env production --name api-env --namespace api
|
|
37
|
+
$ onesecret fly --env production --app my-app
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { run } = require('../src/cli')
|
|
5
|
+
|
|
6
|
+
run(process.argv.slice(2))
|
|
7
|
+
.then(({ stdout, exitCode = 0 }) => {
|
|
8
|
+
if (stdout) console.log(stdout)
|
|
9
|
+
process.exitCode = exitCode
|
|
10
|
+
})
|
|
11
|
+
.catch(error => {
|
|
12
|
+
console.error(error.code ? error.message : error)
|
|
13
|
+
process.exitCode = 1
|
|
14
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,109 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onesecret",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Keep just one secret to decrypt the rest: SOPS + age secrets for Node.js, with no binaries to install.",
|
|
4
|
+
"homepage": "https://github.com/kikobeats/onesecret",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"onesecret": "bin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"author": {
|
|
14
|
+
"email": "josefrancisco.verdu@gmail.com",
|
|
15
|
+
"name": "Jose Francisco 'Kiko' Verdú Gambín",
|
|
16
|
+
"url": "https://kikobeats.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/kikobeats/onesecret.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/kikobeats/onesecret/issues"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"age",
|
|
27
|
+
"config",
|
|
28
|
+
"dotenv",
|
|
29
|
+
"encryption",
|
|
30
|
+
"env",
|
|
31
|
+
"fly",
|
|
32
|
+
"kubernetes",
|
|
33
|
+
"secrets",
|
|
34
|
+
"sops"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"js-yaml": "~5.5.0",
|
|
38
|
+
"mri": "~1.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@commitlint/cli": "latest",
|
|
42
|
+
"@commitlint/config-conventional": "latest",
|
|
43
|
+
"ava": "latest",
|
|
44
|
+
"c8": "latest",
|
|
45
|
+
"dotenv": "~18.0.8",
|
|
46
|
+
"finepack": "latest",
|
|
47
|
+
"git-authors-cli": "latest",
|
|
48
|
+
"github-generate-release": "latest",
|
|
49
|
+
"nano-staged": "latest",
|
|
50
|
+
"simple-git-hooks": "latest",
|
|
51
|
+
"standard": "latest",
|
|
52
|
+
"standard-markdown": "latest",
|
|
53
|
+
"standard-version": "latest"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">= 24"
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"bin",
|
|
60
|
+
"src"
|
|
61
|
+
],
|
|
62
|
+
"preferGlobal": true,
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"ava": {
|
|
65
|
+
"files": [
|
|
66
|
+
"test/**/*.js",
|
|
67
|
+
"!test/helpers/**"
|
|
68
|
+
],
|
|
69
|
+
"timeout": "30s"
|
|
70
|
+
},
|
|
71
|
+
"commitlint": {
|
|
72
|
+
"extends": [
|
|
73
|
+
"@commitlint/config-conventional"
|
|
74
|
+
],
|
|
75
|
+
"rules": {
|
|
76
|
+
"body-max-line-length": [
|
|
77
|
+
0
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"nano-staged": {
|
|
82
|
+
"*.js": [
|
|
83
|
+
"npx @kikobeats/prettier-standard",
|
|
84
|
+
"standard --fix"
|
|
85
|
+
],
|
|
86
|
+
"*.md": [
|
|
87
|
+
"standard-markdown"
|
|
88
|
+
],
|
|
89
|
+
"package.json": [
|
|
90
|
+
"finepack"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"simple-git-hooks": {
|
|
94
|
+
"commit-msg": "npx commitlint --edit",
|
|
95
|
+
"pre-commit": "npx nano-staged"
|
|
96
|
+
},
|
|
97
|
+
"scripts": {
|
|
98
|
+
"clean": "rm -rf node_modules",
|
|
99
|
+
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
100
|
+
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
101
|
+
"lint": "standard-markdown README.md && standard",
|
|
102
|
+
"postrelease": "pnpm release:tags && pnpm release:github && pnpm publish",
|
|
103
|
+
"pretest": "pnpm lint",
|
|
104
|
+
"release": "standard-version -a",
|
|
105
|
+
"release:github": "github-generate-release",
|
|
106
|
+
"release:tags": "git push --follow-tags origin HEAD:master",
|
|
107
|
+
"test": "c8 ava"
|
|
108
|
+
}
|
|
6
109
|
}
|