Protect private files before hosting
Before your first record and publish: what .nexapolicy controls, why labels are repository-specific encryption rather than ignore rules, how to protect .env and credential files with a secrets label, validate the policy, and confirm sealed files before anything is hosted.
You have a project and nothing recorded. Before the first nexa record, decide which
of its files are private and protect them. This page does that end to end, with a secrets label, and ends with the first version recorded and its protection
confirmed. If the project has nothing private, the short path is two commands.
Every command here names the project first — nexa my-project policy validate —
which is the same as nexa policy validate -p my-project. Substitute your own
project name.
Why this comes before hosting
Protection is decided when a version is recorded, and it is never applied to
versions recorded earlier. nexa publish then uploads every version recorded so far. So
a file recorded before its label existed is readable in that version, and hosting it puts that version
on Rinko Hub. Now, before the first record, is the only moment protection costs nothing.
What .nexapolicy controls
~/Development/.nexapolicy — created by nexa root — says which paths of
each project are sealed, and under which label. It is read when
you record: the files it matches are encrypted under the label's key before they are written
into the version, and the policy in force is pinned into that version, so a collaborator
receives the rules with the work. Editing the file later never reinterprets a version already
recorded.
A label is encryption, not an ignore rule. A labelled file is still recorded,
synced and hosted — as ciphertext that only devices holding the label's key can open. A label
never leaves anything out of a version; to keep build output out, use an ignore block (see Leave build output out). The policy file names labels and
never holds a key, so it is safe to read, review and publish.
Labels are repository-specific
Each project under the root is its own repository, and a label belongs to one repository: nexa my-project label create secrets creates secrets in my-project alone, with its own identifier and its own key holders. Another
project's secrets is a different label that happens to share the name.
So the root holds one physical file, but every label rule in it sits inside the
block of the project whose label it names: project "my-project" { … }. A label rule outside every project block — in global — is refused by check, validate, explain and record, because each project would resolve it against its
own, unrelated labels:
policy 1
global {
label "secrets" { paths = ["**/.env*"] }
}nexa my-project policy validateerror: object error
caused by: invalid policy on line 3: labels are repository-specific: `label "secrets"` in `global` would apply to every project in this Development Root and resolve against each repository's own labels; scope it under the project instead: `project "my-project" { label "secrets" { paths = [...] } }`To protect the same kind of file in several projects, create the label in each and give each
project its own block. Earlier releases accepted the global form; a file still using
it is refused until its rules are moved into project blocks, and nothing it would have sealed is recorded
in the meantime.
Labels are not retroactive
A version recorded before a file was labelled keeps that file readable, permanently. Labelling it later seals it in the versions recorded after; history is never rewritten, so the earlier version still holds the plaintext — locally, and on Rinko Hub once published.
If a credential has already been recorded or published unprotected, treat it as exposed. Rotate it — issue a new key, token or password and revoke the old one — and protect the file before recording the replacement. Sealing the file afterwards does not make the old value secret again.
Exact paths and patterns
| You write | It protects | If it matches no file |
|---|---|---|
app.json | Exactly that path, character for character | record refuses, and nothing is recorded |
secrets/** | That directory and everything beneath it | A warning: it may be written ahead of its files |
**/*.pem | Any path of that shape, at any depth | A warning, for the same reason |
An exact path names one file. If it is not there, the rule protects nothing — usually because of a typo — so recording refuses rather than producing a version that only looks protected. A subtree or glob that matches nothing yet is reported and allowed, because it can be written before its files exist. Paths are relative to the project directory.
Protect secrets, step by step
The example project holds .env, app.json, config/private.json, secrets/deploy.key and src/main.ts. Your files will differ: the paths below are examples of what commonly
needs protecting, not a list every project has.
Create the label
nexa my-project label create secretsCreated label secrets.
identifier: d35270b3bcc3891c2c0532d708e06f2f
name: secrets
No paths were given, so nothing is declared yet. Add them in ~/Development/.nexapolicy:
project "my-project" {
label "secrets" { paths = ["<path>"] }
}
then check them with `nexa my-project policy validate`.
The key was granted to this device. Share it with another device using `nexa label grant secrets --device <identity>`, where the identity comes from `nexa device identity` on that machine.The label now exists in my-project's repository, and its key was granted to this
device. Nothing is protected yet: no rule names it.
Confirm this device holds the key
nexa my-project label grants secretsLabel: secrets Identifier: d35270b3bcc3891c2c0532d708e06f2f epoch 0 7a24-4d6c-bfed-9fa9 1 grant(s). A device keeps every epoch it was given: rotation adds an epoch, it never takes one away.
nexa my-project device identityDevice: 7a24-4d6c-bfed-9fa9 Identity: 764d6f11…0d563bf6 Give the identity line to whoever holds a label you need; they run `nexa label grant <label> --device <identity>`.
The device listed under the grant is the Device: line of nexa device identity — this machine holds the key. Another device of yours, or a
collaborator's, gets it with nexa label grant secrets --device <identity>, or
from Rinko Hub; see Sharing a key. A device without
the key cannot read what the label seals.
Add the rules
Open ~/Development/.nexapolicy and add a block for the project. A first draft often contains
mistakes; this one has two:
policy 1
project "my-project" {
label "secrets" {
paths = [
".env",
".app.json",
"credentials.json",
"config/private.json",
"secrets/**",
"**/*.pem",
]
}
}Validate
nexa my-project policy validate~/Development/.nexapolicy is valid (policy version 1).
my-project 6 rule(s) over 1 label(s)
Syntax: valid
Labels: every key resolves; no two rules of equal specificity disagree
Matching: project `my-project`, against the files on disk now
exact paths 2 of 4 match a file
credentials.json secrets 0 files -- protects nothing
.app.json secrets 0 files -- protects nothing
patterns 1 of 2 match a file
**/*.pem secrets 0 files yet
A rule matching no file protects nothing. Check what a path resolves to
with `nexa policy explain <path>`.
error: cannot do that here: the policy is well formed, but an exact path in it matches no file; `nexa record` will refuse until it is correctedTwo exact paths match nothing. credentials.json is simply not in this project — an
example that does not apply here, so remove it. .app.json is a typo for app.json, and nexa policy explain shows what each spelling does:
nexa my-project policy explain app.jsonMatched: no rule Effective label: none -- stored in the clear Source file: .nexapolicy (in the Development Root)
nexa my-project policy explain .app.jsonMatched: rank 3/9/0 .app.json secrets project my-project Effective label: secrets (d35270b3bcc3891c2c0532d708e06f2f) Source file: .nexapolicy (in the Development Root) Key: held by this device
The real file would have been stored in the clear while the rule protected a file that does not exist. Correct the draft:
policy 1
project "my-project" {
label "secrets" {
paths = [
".env",
"app.json",
"config/private.json",
"secrets/**",
"**/*.pem",
]
}
}nexa my-project policy validate~/Development/.nexapolicy is valid (policy version 1).
my-project 5 rule(s) over 1 label(s)
Syntax: valid
Labels: every key resolves; no two rules of equal specificity disagree
Matching: project `my-project`, against the files on disk now
exact paths 3 of 3 match a file
patterns 1 of 2 match a file
**/*.pem secrets 0 files yet
A rule matching no file protects nothing. Check what a path resolves to
with `nexa policy explain <path>`.nexa my-project policy explain app.jsonMatched: rank 3/8/0 app.json secrets project my-project Effective label: secrets (d35270b3bcc3891c2c0532d708e06f2f) Source file: .nexapolicy (in the Development Root) Key: held by this device
Every exact path now names a file. **/*.pem matches nothing yet, which is allowed: it
will protect a certificate the moment one is added.
Record, and confirm what was sealed
nexa my-project record -m "Initial version"warning: 1 policy rule(s) match no file in this workspace, so they protect nothing here at present: `**/*.pem` (secrets) Created version 74f1bb1d5f74 workspace: main files: 5 sealed: 4 Selected on branch main.
sealed: 4 is the check that matters: .env, app.json, config/private.json and secrets/deploy.key. Compare it with the files
you meant to protect before going further. If it is lower than you expected, a file you meant to
protect was recorded readable — fix the rule, rotate whatever that file holds, and record again.
nexa my-project policy showDeclared in ~/Development/.nexapolicy for project `my-project` config/private.json readable secrets 1 file app.json readable secrets 1 file .env readable secrets 1 file secrets/** readable secrets 1 file **/*.pem readable secrets 0 files yet A rule matching 0 files protects nothing. Check a path with `nexa policy explain <path>`.
Leave build output out
record captures every file in the project, so compiled output and dependency
folders — a Rust target/, a node_modules/ — would be recorded too, often
gigabytes of them. Before the first record, ignore them in the same file:
policy 1
global {
ignore { paths = ["**/target", "**/node_modules"] }
}global applies to every project under the root; an ignore block inside
a project block applies to that project alone. Ignored paths are never recorded, never shown by status, and left alone on disk. Ignoring is not protecting — an ignored file is
just absent from the version — so secrets still need a label. Requires Nexa 0.1.7 or later; run nexa update first.
A project with no secrets
Not every project has private files, and none of the above is required. A project with no label and no block validates and records normally, and nothing stops it from being hosted:
nexa docs-site policy validate~/Development/.nexapolicy is valid (policy version 1). my-project 5 rule(s) over 1 label(s) (another repository's labels: validate from that project) Syntax: valid Labels: every key resolves; no two rules of equal specificity disagree Matching: no rule applies to project `docs-site`
nexa docs-site record -m "Initial version"Created version 4d999e16e6f0 workspace: main files: 1 Selected on branch main.
There is no sealed: line because nothing is protected. Every file in this version
will be readable by whoever can read the repository on Rinko Hub — which is what you want when
nothing in it is secret. my-project's block is listed but not resolved here: its
labels are my-project's.
Before you continue
- Every private file in the project is named by a rule in its own project block.
- Build output and dependency folders are covered by an
ignoreblock. nexa <project> policy validatesucceeds.nexa <project> policy explain <path>reports each private file as protected.- The first record's
sealed:count matches the files you meant to protect. - Any credential that was ever recorded unprotected has been rotated.
Then host it on Rinko Hub. For the full grammar, see Labels, workspaces and .nexapolicy; for what a device without the key sees, Privacy and file control.