Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .vitepress/_sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
{ text: 'Configuration', link: '/settings/configuration' },
{ text: 'TLS & Certificates', link: '/settings/tls' },
{ text: 'Secrets', link: '/settings/secrets' },
{ text: 'Vault', link: '/settings/vault' },
{ text: 'Seed', link: '/settings/seed' },
]
},
{
Expand All @@ -70,7 +70,6 @@ export default {
{ text: 'Kind', link: '/tools/kind' },
{ text: 'Python', link: '/tools/python' },
{ text: 'Rust', link: '/tools/rust' },
{ text: 'Seed', link: '/tools/seed' },
{ text: 'SSH', link: '/tools/ssh' },
{ text: 'ws-cli', link: '/tools/ws-cli' },
]
Expand Down
4 changes: 2 additions & 2 deletions docs/editor/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ docker volume ls
Several workspace features store their configuration under `~/.ws` (`/home/kloud/.ws`),
including [sideloaded extensions](/editor/extensions), [startup and session scripts](/settings/autoload-scripts),
[drop-in CA certificates](/settings/tls#drop-in-directory-ws-ca-d),
the [secrets vault](/settings/secrets), and your shell and REPL history (`~/.ws/history`).
the [seed source](/settings/seed), and your shell and REPL history (`~/.ws/history`).

Because this directory lives inside the container, its contents are lost on restart unless
you persist it separately:
Expand Down Expand Up @@ -114,7 +114,7 @@ volumeMounts:
subPath: workspace

- name: data
mountPath: /home/kloud/.ws # also persists shell/REPL history (~/.ws/history)
mountPath: /home/kloud/.ws
subPath: ws

- name: data
Expand Down
File renamed without changes
5 changes: 5 additions & 0 deletions docs/public/icons/seed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions docs/settings/secrets.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
description: "Manage sensitive credentials in Kloud Workspace with the ws-cli secrets command β€” encrypt passwords, API keys, and SSH keys instead of storing plaintext."
see:
- name: Vault
link: /settings/vault
- name: Seed
link: /settings/seed
- name: ws secrets Command Reference
link: /tools/ws-cli#secrets-ws-secrets
target: _self
Expand All @@ -13,7 +13,7 @@ see:

# Secrets

![Vault](/icons/vault.svg){.doc-image}
![Secrets](/icons/secrets.svg){.doc-image}

Kloud Workspace applications often require sensitive credentials: database passwords, API
keys, SSH keys, and authentication tokens.
Expand Down Expand Up @@ -78,10 +78,10 @@ echo "my-secret" | ws secrets encrypt - --master .master.key
cat encrypted.txt | ws secrets decrypt - --master .master.key
```

## Vault
## Seed

For declarative bulk secret injection using YAML vault files, see the
[Vault documentation](/settings/vault).
For declarative secret projection β€” inline `${secrets.NAME}` values and whole-file `secret: true`
entries in the seed manifest β€” see the [seed documentation](/settings/seed).

## Authentication Passwords

Expand Down
287 changes: 287 additions & 0 deletions docs/settings/seed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
---
description: Project files and secrets from a durable seed directory onto the workspace filesystem on every boot, within an ownership boundary.
see:
- name: Secrets
link: /settings/secrets
- name: Autoload Scripts
link: /settings/autoload-scripts
---

# Seed

![Seed](/icons/seed.svg){.doc-image}

Seeding projects files and secrets from a durable source directory onto the filesystem on every
container boot.
Fill the source once and it re-projects each restart, with no hand-run setup scripts.

One source tree carries two tiers: plain **bare files** copied verbatim, and a **`.seed.yaml`
manifest** that layers merge, templating, and secrets over them.

## At a Glance

- **Source:** <EnvVar group="seed" name="source" /> *(default `~/.ws/seed.d`)*. An empty or absent
directory is a clean no-op.
- **Manifest:** a single `<source>/.seed.yaml` at the source root, excluded from the bare mirror.
- **Runs:** at boot, before the workspace's own configuration steps.
- **Writes:** only where you own the destination, anywhere your account owns the nearest existing
parent directory.

## Bare Files: FS-Rooted Mirror

The path under the source maps directly onto the root filesystem:

```text
~/.ws/seed.d/home/kloud/.gitconfig β†’ ~/.gitconfig
~/.ws/seed.d/etc/workspace/x β†’ /etc/workspace/x
```

A home file therefore lives at `seed.d/home/kloud/<path>`, not at the source root.
Bare files copy verbatim with mode `644` *(new directories `755`)* and never write if the
destination already exists, unless forced.

## The Manifest

A single hidden `.seed.yaml` at the source root declares behaviors.
It opens with a `version` key and two sections:

```yaml
version: v1

secrets:
GH_TOKEN: kZ9... # inline ciphertext, or file:/run/secrets/gh_token

seeds:
~/.gitconfig:
op: merge

~/.ssh/id_ed25519:
secret: true

~/.zshenv:
op: append
content: |
export EDITOR=nano
```

The `seeds:` map is **keyed by destination**, `~`, `${ws_home}`, `${ws_server_root}` and
`${ws_user}` expand.

The source for each entry is implied by that key: the **rhyming mirror file**
*(`<source>/<dest-without-leading-slash>`)*, or an inline `content:` literal.
There is no `file:` pointer inside `seeds:`.

Each entry must carry at least one behavior, `secret`, `mode`, a non-`copy` `op`, or `template`.
A plain copy belongs in the mirror tier, so a copy-only manifest entry is a parse error.

When a destination is produced by both a bare file and a manifest entry, the manifest entry wins.

## Operations

`op` is one of `copy` *(default)*, `merge`, `append`, `prepend`, `block` or `lineinfile`.

`merge` deep-merges structured data, with the format inferred from the destination extension
*(`.json`, `.yaml`, `.toml`)*.
Maps merge recursively, scalars override, and **lists replace** wholesale.
A scalar-versus-map conflict at a key is a hard error that leaves the destination unchanged.

```yaml
seeds:
~/.config/app/config.json:
op: merge
content: '{"telemetry": false}'
```

`append` and `prepend` add `content` to the end or start of the destination.

::: warning

`append` and `prepend` follow the same **write-if-absent** rule as every entry
*(see [Force and Ephemerality](#force-and-ephemerality))*: an existing destination is skipped unless forced.

Forced, they re-apply on every boot *(**naive, not idempotent**)*, so the content is added again each
time and accumulates *(if a persistent volume is mounted)*.

To add a single line idempotently, use [`op: lineinfile`](#managed-lines); to manage a region, use
[`op: block`](#managed-blocks).

:::

### Managed Blocks

`op: block` manages a marked region inside a file.

It wraps `content` between two marker lines and reconciles that block on every boot, idempotently,
even when the content changes:

```yaml
seeds:
~/.zshenv:
op: block
content: "export EDITOR=nano\n"
```

The first apply appends the block and inserts the markers for you:

```text
# >>> ws-seed >>>
export EDITOR=nano
# <<< ws-seed <<<
```

Later applies find those markers and replace only the body between them, so the region never
duplicates and tracks content changes.

You never write the markers by hand.
A missing file is created; an existing file keeps its content and gains the block at the end.

`block` ignores `force`: it is safe to re-run, and rewrites the file only when the block's contents
change. Malformed markers *(a begin without an end, or markers out of order)* are a hard error that
leaves the destination unchanged.

The marker text is fixed, but its comment prefix defaults to `#` and is set per entry with `comment`
for files where `#` is not a comment:

```yaml
seeds:
~/.config/app/config.js:
op: block
comment: //
content: "module.exports = { telemetry: false }\n"
```

```text
// >>> ws-seed >>>
module.exports = { telemetry: false }
// <<< ws-seed <<<
```

`block` is plain text and does not infer a file's comment syntax; `comment` is valid only with
`op: block`. For structured files, prefer `merge` over a marked block.

### Managed Lines

`op: lineinfile` manages a single line, matched by its key, idempotently:

```yaml
seeds:
~/.zshenv:
op: lineinfile
content: "export EDITOR=nano\n"
```

The key is the text up to and including the first `=` *(here `export EDITOR=`)*. On each apply a
line with that key is replaced in place; if none exists the line is appended, creating the file if
needed. The content is exactly one line, so an interior newline is a hard error *(use `op: block`
for a multi-line region)*.

Like `block`, `lineinfile` ignores `force` and rewrites the file only when the line changes. It is
the idempotent counterpart to `append` for `key=value` files such as `~/.zshenv`.

## Templating

Set `template: true` to substitute a closed variable set in the source before writing:

```yaml
seeds:
~/.config/app/env:
template: true
content: "HOME=${ws_home}\nTOKEN=${secrets.GH_TOKEN}\n"
```

The available tokens are `${ws_home}`, `${ws_user}`, `${ws_server_root}` and `${secrets.NAME}`. An
unknown `${...}` token is a hard error. There is no expression language and no escape syntax. To
emit a literal `${...}`, leave `template` unset.

## Secrets

Two secret shapes share one ciphertext format, both produced by
[`ws-cli secrets encrypt`](/settings/secrets):

- **Inline values:** live in the top-level `secrets:` map *(`NAME: <ciphertext>` or `NAME: file:<path>`)*
and are referenced only through `${secrets.NAME}` in a `template: true` entry.
- **Whole-file secrets:** set `secret: true` on the entry. Its source
*(the rhyming mirror ciphertext file, or an inline `content:` ciphertext)* is decrypted and
written as plaintext.

A secret-bearing output is forced to mode `0600`, and its cleartext never reaches logs.
A secret that will not decrypt *(missing key, corrupt ciphertext)* is skipped with a warning and
nothing is written, never the ciphertext, never a partial.

A manifest with no secrets needs no key.

::: warning

Keep ciphertext files **outside** the mirror tree unless a manifest entry claims them.
An undeclared encrypted file in the source is copied verbatim as ciphertext, exactly like any other
bare file.

:::

::: info

To change the master key, run [`ws-cli seed rotate`](#apply-and-inspect) β€” it re-encrypts every
managed ciphertext in place under the new key.

:::

## Ownership Boundary

Seed writes only to destinations you own, anywhere your account owns the nearest existing parent
directory: `~/.ssh`, `~/.ws/startup.d`, dotfiles, a `/opt/mine` you created. Anything else is skipped
with a warning.

There is no `sudo` and no escalation: a location you do not own simply fails the check, which is also
exactly where you could not write by hand.

::: tip

A write into `~/.ws/{startup.d,ca.d,session.d,features.d}` is allowed and emits a notice. Seeding
your own startup script is a legitimate use. Mark it executable if it needs to run.

:::

## Force and Ephemerality

The default is **write-if-absent**: an entry writes only when the destination is missing.
This re-seeds ephemeral paths every boot for free and preserves a hand-edited persistent file.
To overwrite an existing destination, set `force: true` on the entry or pass `--force` to re-apply
everything.
For `merge`, `force` gates the merge too, and without it an existing destination is left alone.

## Apply and Inspect

The boot hook runs `ws-cli seed apply` with no arguments.
Run it by hand to re-project, or scope it to specific destinations:

```sh
# Project everything
ws-cli seed apply

# Re-apply, overwriting existing destinations
ws-cli seed apply --force

# Project a single destination
ws-cli seed apply ~/.gitconfig

# List the resolved plan
ws-cli seed ls
```

A named destination matches its entry regardless of `~`, `$HOME` or absolute form.

Rotate the master key across every managed ciphertext β€” the `secrets:` map, its `file:` targets, and
every `secret: true` source β€” in one pass:

```sh
# Re-encrypt everything from the current key to a new one
ws-cli seed rotate --master <current> --new-master <new>
```

::: warning

`seed rotate` rewrites ciphertext **in place** β€” there is no dry-run and no backup. It fails closed:
every secret is decrypted under the current key *before* anything is written, so a wrong key changes
nothing. If a run is interrupted, re-run it with the new key to finish.

:::
2 changes: 1 addition & 1 deletion docs/settings/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ docker run \
This is the recommended path for ad-hoc certificate injection on a
running workspace β€” the same persisted volume already used for
[autoload scripts](/settings/autoload-scripts), extensions, and the
[secrets vault](/settings/vault).
[seed source](/settings/seed).

### Install Certificate from HTTPS Endpoint

Expand Down
Loading
Loading