kitsune-kit 0.4.1 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +143 -2
- data/CONTRIBUTING.md +83 -0
- data/README.md +112 -135
- data/SECURITY.md +25 -0
- data/bin/kit +4 -1
- data/docs/architecture/decisions/0001-product-boundary.md +23 -0
- data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
- data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
- data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
- data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
- data/docs/architecture/decisions/0006-tui-backend.md +21 -0
- data/docs/architecture/decisions/0007-compose-customization.md +30 -0
- data/docs/architecture/provider-adapters.md +49 -0
- data/docs/architecture.md +93 -0
- data/docs/commands.md +224 -0
- data/docs/configuration.md +226 -0
- data/docs/getting-started.md +133 -0
- data/docs/providers/digitalocean.md +52 -0
- data/docs/releasing.md +43 -0
- data/docs/roadmap.md +46 -0
- data/docs/security-audit.md +130 -0
- data/docs/security.md +116 -0
- data/docs/services/compose.md +164 -0
- data/docs/services/postgres.md +78 -0
- data/docs/services/redis.md +53 -0
- data/docs/testing.md +200 -0
- data/docs/troubleshooting.md +123 -0
- data/docs/tui.md +57 -0
- data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
- data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
- data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
- data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
- data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
- data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
- data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
- data/lib/kitsune/kit/adapters/provider.rb +34 -0
- data/lib/kitsune/kit/adapters/transport.rb +22 -0
- data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
- data/lib/kitsune/kit/application.rb +170 -0
- data/lib/kitsune/kit/cancellation.rb +21 -0
- data/lib/kitsune/kit/cli.rb +879 -64
- data/lib/kitsune/kit/clock.rb +11 -0
- data/lib/kitsune/kit/configuration.rb +577 -0
- data/lib/kitsune/kit/errors.rb +98 -0
- data/lib/kitsune/kit/events.rb +55 -0
- data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
- data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
- data/lib/kitsune/kit/operations/ensure_service.rb +302 -0
- data/lib/kitsune/kit/operations/remote_script.rb +214 -0
- data/lib/kitsune/kit/operations/service_backup.rb +71 -0
- data/lib/kitsune/kit/operations/service_files.rb +131 -0
- data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
- data/lib/kitsune/kit/operations/service_state.rb +59 -0
- data/lib/kitsune/kit/plan.rb +72 -0
- data/lib/kitsune/kit/reporters/human.rb +89 -0
- data/lib/kitsune/kit/reporters/json.rb +65 -0
- data/lib/kitsune/kit/reporters/reporter.rb +11 -0
- data/lib/kitsune/kit/result.rb +28 -0
- data/lib/kitsune/kit/run_journal.rb +102 -0
- data/lib/kitsune/kit/run_logger.rb +37 -0
- data/lib/kitsune/kit/scripts/docker.sh +142 -0
- data/lib/kitsune/kit/scripts/firewall.sh +210 -0
- data/lib/kitsune/kit/scripts/metrics.sh +59 -0
- data/lib/kitsune/kit/scripts/ssh.sh +95 -0
- data/lib/kitsune/kit/scripts/swap.sh +86 -0
- data/lib/kitsune/kit/scripts/unattended.sh +86 -0
- data/lib/kitsune/kit/scripts/user.sh +114 -0
- data/lib/kitsune/kit/secret_filter.rb +54 -0
- data/lib/kitsune/kit/secret_store.rb +32 -0
- data/lib/kitsune/kit/secret_stores/store.rb +12 -0
- data/lib/kitsune/kit/service_compose.rb +272 -0
- data/lib/kitsune/kit/state_store.rb +158 -0
- data/lib/kitsune/kit/state_stores/store.rb +15 -0
- data/lib/kitsune/kit/tui/actions.rb +72 -0
- data/lib/kitsune/kit/tui/application.rb +35 -0
- data/lib/kitsune/kit/tui/controller.rb +162 -0
- data/lib/kitsune/kit/tui/renderer.rb +134 -0
- data/lib/kitsune/kit/tui/state.rb +18 -0
- data/lib/kitsune/kit/tui/store.rb +88 -0
- data/lib/kitsune/kit/tui/terminal.rb +95 -0
- data/lib/kitsune/kit/version.rb +1 -1
- data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
- data/lib/kitsune/kit/workflows/base.rb +31 -0
- data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
- data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
- data/lib/kitsune/kit/workflows/doctor.rb +225 -0
- data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
- data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
- data/lib/kitsune/kit/workflows/import_server.rb +100 -0
- data/lib/kitsune/kit/workflows/initialize_project.rb +129 -0
- data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
- data/lib/kitsune/kit/workflows/rollback.rb +54 -0
- data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
- data/lib/kitsune/kit.rb +42 -2
- metadata +125 -79
- data/.rspec +0 -3
- data/Rakefile +0 -8
- data/kitsune-kit-logo.jpg +0 -0
- data/lib/kitsune/blueprints/.env.template +0 -31
- data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
- data/lib/kitsune/blueprints/docker/redis.yml +0 -23
- data/lib/kitsune/blueprints/kit.env.template +0 -1
- data/lib/kitsune/kit/ansi_color.rb +0 -78
- data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
- data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
- data/lib/kitsune/kit/commands/dns.rb +0 -112
- data/lib/kitsune/kit/commands/init.rb +0 -148
- data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
- data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
- data/lib/kitsune/kit/commands/provision.rb +0 -43
- data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
- data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
- data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
- data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
- data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
- data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
- data/lib/kitsune/kit/commands/setup_user.rb +0 -189
- data/lib/kitsune/kit/commands/ssh.rb +0 -46
- data/lib/kitsune/kit/commands/switch_env.rb +0 -42
- data/lib/kitsune/kit/defaults.rb +0 -91
- data/lib/kitsune/kit/env_loader.rb +0 -41
- data/lib/kitsune/kit/options_builder.rb +0 -26
- data/lib/kitsune/kit/provisioner.rb +0 -107
- data/sig/kitsune/kit.rbs +0 -6
data/docs/security.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
Kitsune Kit changes remote security policy and can destroy infrastructure. Its design favors explicit ownership, verified transitions and refusal over implicit adoption.
|
|
4
|
+
|
|
5
|
+
## Trust boundaries
|
|
6
|
+
|
|
7
|
+
Kitsune Kit trusts:
|
|
8
|
+
|
|
9
|
+
- the local machine/user running the gem;
|
|
10
|
+
- configuration committed by the project;
|
|
11
|
+
- secrets supplied through the process environment;
|
|
12
|
+
- the independently verified first SSH host-key fingerprint;
|
|
13
|
+
- provider and package repositories over TLS;
|
|
14
|
+
- local state as the record of which resources Kitsune Kit owns.
|
|
15
|
+
|
|
16
|
+
Compromise of the local account, repository, environment, private key or state can invalidate these assumptions. Kitsune Kit is not a secrets vault and does not encrypt local files.
|
|
17
|
+
|
|
18
|
+
## Secrets
|
|
19
|
+
|
|
20
|
+
Never place API tokens or service passwords in YAML, command arguments, state or Git. Configuration stores only environment-variable names. Secret values are registered with a central redactor before event reporting/logging.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export DO_API_TOKEN="..."
|
|
24
|
+
export POSTGRES_PASSWORD="..."
|
|
25
|
+
export REDIS_PASSWORD="..."
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Generated service `.env` files exist only on the remote server with mode `0600`. Compose output, fingerprints and local state do not contain plaintext passwords. Newlines, carriage returns and NUL bytes are rejected.
|
|
29
|
+
|
|
30
|
+
Process environments may be observable by privileged local processes and CI administrators. Use the secret store of your CI platform, limit token scope, rotate credentials and avoid shell tracing.
|
|
31
|
+
|
|
32
|
+
## SSH
|
|
33
|
+
|
|
34
|
+
Kitsune Kit never uses `verify_host_key: :never` in production adapters. A new key raises a prompt containing the exact SHA256 fingerprint. Acceptance records it in `.kitsune/known_hosts`; later sessions use strict matching.
|
|
35
|
+
|
|
36
|
+
`--trust-host-key` accepts only an exact fingerprint and is intended for independently verified non-interactive bootstrap. A mismatch fails. Key changes are not automatically accepted.
|
|
37
|
+
|
|
38
|
+
SSH hardening preserves a recovery route:
|
|
39
|
+
|
|
40
|
+
1. bootstrap using deploy or root access;
|
|
41
|
+
2. create/configure the deploy user;
|
|
42
|
+
3. verify a second deploy-user connection;
|
|
43
|
+
4. validate generated `sshd_config` with `sshd -t`;
|
|
44
|
+
5. reload first with key-based root recovery still enabled;
|
|
45
|
+
6. verify a new deploy-user connection, then disable root and verify again;
|
|
46
|
+
7. add the new firewall route before removing a stale route owned by Kitsune Kit and verify deploy access once more.
|
|
47
|
+
|
|
48
|
+
The transport keeps the already authenticated session open across each safety-critical transition. Fresh verification uses a separate connection; if it fails before or after finalization, the preserved session runs the script rollback before it closes. A recovery failure is surfaced as a verification error with provider-console guidance. Kitsune Kit does not modify unrelated SSH/firewall configuration during rollback.
|
|
49
|
+
|
|
50
|
+
## Command and file safety
|
|
51
|
+
|
|
52
|
+
Remote commands are a validated executable plus separately Shellwords-escaped arguments. File uploads are base64 data over stdin, not interpolated shell source. Absolute remote paths and modes are validated before connecting.
|
|
53
|
+
|
|
54
|
+
Generated Compose documents are built as Ruby hashes and serialized with YAML. Resource names, users, ports, CIDRs, images, domains and environment names have allow-list validation. Tests include metacharacters, substitutions, traversal and embedded newline attacks.
|
|
55
|
+
|
|
56
|
+
Versioned shell scripts use `set -Eeuo pipefail`, explicit action dispatch and verification. ShellCheck runs in CI.
|
|
57
|
+
|
|
58
|
+
## Firewall and data services
|
|
59
|
+
|
|
60
|
+
PostgreSQL and Redis do not publish host ports by default. When publishing is enabled:
|
|
61
|
+
|
|
62
|
+
- at least one valid allowed CIDR is required;
|
|
63
|
+
- Compose binds only the configured IPv4 address/port;
|
|
64
|
+
- matching allow and terminal drop rules are installed in Docker's `DOCKER-USER` chain because Docker-published ports can bypass UFW forwarding policy;
|
|
65
|
+
- only rules recorded as created by Kitsune Kit are reconciled or removed.
|
|
66
|
+
|
|
67
|
+
Use private networking or an application-local Docker network whenever possible. Never publish a database to `0.0.0.0` with a broad CIDR unless the exposure is deliberate and independently audited.
|
|
68
|
+
|
|
69
|
+
## Ownership and destructive actions
|
|
70
|
+
|
|
71
|
+
Kitsune Kit records provider IDs, DNS record IDs, remote fingerprints, file backups and firewall ownership. Same-name resources without the expected tags/state are not adopted or deleted.
|
|
72
|
+
|
|
73
|
+
Semantics:
|
|
74
|
+
|
|
75
|
+
| Operation | Server | Containers | Data volume | Configuration |
|
|
76
|
+
| --- | --- | --- | --- | --- |
|
|
77
|
+
| `rollback` | Keep | Restore/remove managed | Keep | Restore captured state |
|
|
78
|
+
| service `remove` | Keep | Remove | Keep | Keep ownership record |
|
|
79
|
+
| service `destroy-data` | Keep | Remove | Delete | Delete managed service files/state |
|
|
80
|
+
| server `destroy` | Delete | Delete with server | Delete with server | Restore/remove managed DNS after provider deletion |
|
|
81
|
+
|
|
82
|
+
`--yes` never authorizes permanent service data or server destruction. Exact confirmation strings are required.
|
|
83
|
+
Interactive service-data destruction offers a restricted archive first. In automation,
|
|
84
|
+
`--backup-before-destroy` requests it explicitly; copy that same-server archive to independent storage.
|
|
85
|
+
|
|
86
|
+
The deploy user belongs to the `docker` group and has passwordless sudo, so it is effectively a privileged
|
|
87
|
+
administrator. Protect its private key as a root credential, restrict SSH CIDRs where possible and rotate the
|
|
88
|
+
key after suspected exposure.
|
|
89
|
+
|
|
90
|
+
## State, logs and diagnostics
|
|
91
|
+
|
|
92
|
+
State/log/support directories and files use restrictive permissions. State writes use file locks, fsync, atomic rename and backups. Logs are structured, redacted and rotated. Support bundles are local JSON and never transmitted.
|
|
93
|
+
|
|
94
|
+
Back up `.kitsune/state/` securely. If state is lost, recover it rather than recreating ownership from names. Inspect support bundles before sharing; redaction is defense in depth, not proof that arbitrary user-provided text contains no sensitive information.
|
|
95
|
+
|
|
96
|
+
When both state copies are lost, `kit server import` is the limited recovery path. It requires an exact numeric provider ID and exact-name confirmation, verifies the complete configured server identity, and imports only server metadata. It deliberately does not claim remote resources or DNS records.
|
|
97
|
+
|
|
98
|
+
## Supply chain
|
|
99
|
+
|
|
100
|
+
- Docker is installed from Docker's official signed apt repository.
|
|
101
|
+
- Metrics installation is off by default. Enabling it requires an explicitly configured SHA256 for the downloaded script; no `curl | sh` pipeline is used.
|
|
102
|
+
- Ruby dependencies are locked for development, audited with Bundler Audit and updated through Dependabot.
|
|
103
|
+
- Release metadata requires MFA on RubyGems. Releases should use trusted publishing and protected tags when configured.
|
|
104
|
+
|
|
105
|
+
Container service tags are mutable unless pinned by digest. For production reproducibility, use an image reference ending in `@sha256:...`, test it in staging and update deliberately.
|
|
106
|
+
|
|
107
|
+
The internal Alpine image used only to archive service volumes is multi-platform and pinned to the published Docker Official Image manifest digest. Its source is the [Docker Hub `alpine:3.20` manifest](https://hub.docker.com/layers/library/alpine/3.20/images/sha256-ac77ebc035f69184acb2660028580c9053f6d0f892de7933e1456d8b5e0ac085). Updating it requires reviewing the new manifest and running backup/restore tests.
|
|
108
|
+
|
|
109
|
+
Kitsune Kit contains no telemetry or automatic upload path. Logs and support bundles remain local unless the
|
|
110
|
+
operator deliberately shares them.
|
|
111
|
+
|
|
112
|
+
The latest manual review and its limitations are recorded in [Security audit](security-audit.md).
|
|
113
|
+
|
|
114
|
+
## Reporting vulnerabilities
|
|
115
|
+
|
|
116
|
+
Follow [SECURITY.md](../SECURITY.md). Do not include real credentials, addresses or domains in a public issue or reproduction.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Compose customization
|
|
2
|
+
|
|
3
|
+
Kitsune Kit can manage PostgreSQL and Redis with a generated Compose document, a user overlay, or a complete
|
|
4
|
+
custom document. All three modes use the same `plan`, `apply`, state, recovery, rollback and data-safety workflow.
|
|
5
|
+
The TUI is optional; these features are fully available through ordinary CLI commands.
|
|
6
|
+
|
|
7
|
+
## Choosing a mode
|
|
8
|
+
|
|
9
|
+
| Mode | Kitsune Kit generates the base | Your file | Best use |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| `generated` | Yes | None | Secure defaults with image, port and password settings controlled by `config.yml`. |
|
|
12
|
+
| `overlay` | Yes | Compose override | Add labels, resource limits, logging, environment options or other supported overrides. |
|
|
13
|
+
| `custom` | No | Complete Compose document | Take direct ownership of the service definition while retaining Kitsune Kit lifecycle management. |
|
|
14
|
+
|
|
15
|
+
Start with `generated`. Prefer `overlay` while the base service, volume, network and health check still fit. Use
|
|
16
|
+
`custom` when you need to replace those assumptions. There is no legacy-blueprint compatibility layer.
|
|
17
|
+
|
|
18
|
+
## Generated mode
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
services:
|
|
22
|
+
postgres:
|
|
23
|
+
compose:
|
|
24
|
+
mode: generated
|
|
25
|
+
file:
|
|
26
|
+
allow_unsafe: false
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The generated document is deterministic. Passwords appear only as `${POSTGRES_PASSWORD}` or
|
|
30
|
+
`${REDIS_PASSWORD}` references; their values are uploaded separately in a mode-`0600` `.env` file.
|
|
31
|
+
|
|
32
|
+
## Overlay mode
|
|
33
|
+
|
|
34
|
+
Create a project file such as `.kitsune/compose/postgres.override.yml`:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
services:
|
|
38
|
+
postgres:
|
|
39
|
+
environment:
|
|
40
|
+
LOG_STATEMENT: ddl
|
|
41
|
+
logging:
|
|
42
|
+
options:
|
|
43
|
+
max-size: 20m
|
|
44
|
+
max-file: "5"
|
|
45
|
+
deploy:
|
|
46
|
+
resources:
|
|
47
|
+
limits:
|
|
48
|
+
memory: 1G
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Reference it from configuration:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
services:
|
|
55
|
+
postgres:
|
|
56
|
+
compose:
|
|
57
|
+
mode: overlay
|
|
58
|
+
file: .kitsune/compose/postgres.override.yml
|
|
59
|
+
allow_unsafe: false
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Kitsune Kit uploads `compose.yml` followed by `compose.override.yml` and supplies both to every Docker Compose
|
|
63
|
+
command in that order. Changes to either document change the plan fingerprint.
|
|
64
|
+
|
|
65
|
+
## Custom mode and eject
|
|
66
|
+
|
|
67
|
+
Create a complete starting document automatically:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
kit service postgres compose eject
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This operation:
|
|
74
|
+
|
|
75
|
+
1. writes `.kitsune/compose/postgres.yml` from the current generated definition;
|
|
76
|
+
2. copies `.kitsune/config.yml` to `.kitsune/config.yml.backup`;
|
|
77
|
+
3. changes the service to `compose.mode: custom` and references the new file.
|
|
78
|
+
|
|
79
|
+
Existing output or backup files are not overwritten. Review both and use `--force` only when replacement is
|
|
80
|
+
intentional. YAML comments in `config.yml` may be reformatted by ejection; the byte-for-byte backup is retained.
|
|
81
|
+
|
|
82
|
+
A custom PostgreSQL file must contain `services.postgres`; Redis must contain `services.redis`. Configuration
|
|
83
|
+
fields such as `image` no longer alter a custom document, but `password_env`, publication/firewall policy and
|
|
84
|
+
lifecycle state remain part of Kitsune Kit's safety model.
|
|
85
|
+
|
|
86
|
+
## Inspecting and validating
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
kit service postgres compose show
|
|
90
|
+
kit service postgres compose validate
|
|
91
|
+
kit service postgres compose diff
|
|
92
|
+
kit service postgres compose show --format json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `show` displays the exact ordered documents and their sources.
|
|
96
|
+
- `validate` parses YAML, validates the required service and applies local security policy.
|
|
97
|
+
- `diff` compares desired content with the last successfully applied Compose fingerprint.
|
|
98
|
+
- `plan` includes mode, ordered files, findings and the overall service fingerprint.
|
|
99
|
+
- `apply` uploads restricted files and runs `docker compose config --quiet` remotely before `up`.
|
|
100
|
+
|
|
101
|
+
Local validation does not replace Docker's own validation. A customization can pass local checks but fail because
|
|
102
|
+
an option is unsupported by the Docker Compose version on the VPS; `apply` then recovers the previous managed
|
|
103
|
+
files and attempts to restart the previous file set.
|
|
104
|
+
|
|
105
|
+
## File boundary
|
|
106
|
+
|
|
107
|
+
Customization files must:
|
|
108
|
+
|
|
109
|
+
- be inside `--root` after path expansion and resolution;
|
|
110
|
+
- be regular files, not symlinks;
|
|
111
|
+
- be at most 256 KiB;
|
|
112
|
+
- contain a top-level YAML mapping;
|
|
113
|
+
- avoid YAML aliases.
|
|
114
|
+
|
|
115
|
+
These constraints keep the deployment input reviewable and prevent a project configuration from reading an
|
|
116
|
+
arbitrary local file.
|
|
117
|
+
|
|
118
|
+
## Secrets
|
|
119
|
+
|
|
120
|
+
Never put a secret value in Compose YAML. Sensitive mapping keys and `KEY=value` environment-list entries are
|
|
121
|
+
rejected unless the value is a `${VARIABLE_NAME}` reference.
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
services:
|
|
125
|
+
postgres:
|
|
126
|
+
environment:
|
|
127
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Export the variable before `doctor`, `plan`, `apply` or `install`. Compose display/JSON output never resolves its
|
|
131
|
+
value. The remote `.env`, Compose documents and backups use restricted permissions.
|
|
132
|
+
|
|
133
|
+
## Unsafe options
|
|
134
|
+
|
|
135
|
+
The local policy flags options that cross Kitsune Kit's normal isolation boundary, including:
|
|
136
|
+
|
|
137
|
+
- privileged containers and host PID/IPC/network namespaces;
|
|
138
|
+
- `ALL`, `SYS_ADMIN`, `SYS_PTRACE` or `NET_ADMIN` capabilities;
|
|
139
|
+
- host devices, absolute bind mounts and the Docker socket;
|
|
140
|
+
- ports outside the exact configured bind/port/firewall model;
|
|
141
|
+
- remote `build` contexts and unmanaged `env_file` references.
|
|
142
|
+
|
|
143
|
+
By default, any finding stops `show`, `validate`, `plan` and `apply`. If the configuration is intentionally outside
|
|
144
|
+
the standard boundary, review every finding and set:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
compose:
|
|
148
|
+
mode: custom
|
|
149
|
+
file: .kitsune/compose/postgres.yml
|
|
150
|
+
allow_unsafe: true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This is an escape hatch, not a promise that Kitsune Kit manages or reverses the extra host resources. Findings
|
|
154
|
+
remain visible in metadata and plan details. Inline secrets are always rejected and cannot be enabled by
|
|
155
|
+
`allow_unsafe`.
|
|
156
|
+
|
|
157
|
+
## Rollback behavior
|
|
158
|
+
|
|
159
|
+
State records the mode, ordered filenames and content fingerprint after a verified apply. Before an update,
|
|
160
|
+
Kitsune Kit backs up all currently tracked and desired Compose files plus `.env`. A failed apply removes the new
|
|
161
|
+
file set, restores the backup and starts the prior ordered file set. `kit rollback` uses the same recorded set.
|
|
162
|
+
|
|
163
|
+
Docker volumes remain governed by the existing service contract: `remove` preserves data and `destroy-data`
|
|
164
|
+
requires exact confirmation. Compose customization does not broaden deletion ownership.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# PostgreSQL service
|
|
2
|
+
|
|
3
|
+
PostgreSQL is an optional Docker Compose service managed by Kitsune Kit on the target server. It is disabled and private by default.
|
|
4
|
+
|
|
5
|
+
## Enable
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
services:
|
|
9
|
+
postgres:
|
|
10
|
+
enabled: true
|
|
11
|
+
image: postgres:17
|
|
12
|
+
publish: false
|
|
13
|
+
bind: 127.0.0.1
|
|
14
|
+
allowed_cidrs: []
|
|
15
|
+
port: 5432
|
|
16
|
+
password_env: POSTGRES_PASSWORD
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export POSTGRES_PASSWORD="$(ruby -rsecurerandom -e 'print SecureRandom.base64(36)')"
|
|
21
|
+
kit plan
|
|
22
|
+
kit service postgres install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The Compose project is `kitsune-ENV-postgres`, attached to the external `kitsune-private` network. Data is stored in its named `data` volume. The database name is `app_ENV` with `-` converted to `_`; the user is `postgres`.
|
|
26
|
+
|
|
27
|
+
## Connectivity
|
|
28
|
+
|
|
29
|
+
With `publish: false`, there is no host port mapping. Applications on `kitsune-private` can connect using Docker service/network addressing and the configured credentials.
|
|
30
|
+
|
|
31
|
+
Publishing is an explicit exception:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
publish: true
|
|
35
|
+
bind: 0.0.0.0
|
|
36
|
+
allowed_cidrs:
|
|
37
|
+
- 203.0.113.10/32
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Kitsune Kit requires at least one CIDR and installs owned `DOCKER-USER` allow/drop rules. Validate the result externally; UFW alone does not reliably restrict Docker-published ports.
|
|
41
|
+
|
|
42
|
+
## Status, backup and removal
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
kit service postgres status
|
|
46
|
+
kit service postgres backup
|
|
47
|
+
kit service postgres remove --yes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Backup pauses the service, archives the named volume into a restricted remote backup directory and unpauses in an `ensure` path. The returned path is on the server. Copy it to independent storage and test restoration; creating an archive on the same Droplet is not disaster recovery.
|
|
51
|
+
|
|
52
|
+
`remove` runs Compose down without `--volumes`, removes only firewall rules owned by Kitsune Kit and marks the service removed. Data remains.
|
|
53
|
+
|
|
54
|
+
## Permanent destruction
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
kit service postgres backup
|
|
58
|
+
kit service postgres destroy-data --backup-before-destroy --confirm-destroy postgres@production
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
In an interactive terminal, `destroy-data` offers to create the archive and then requires typing
|
|
62
|
+
`postgres@ENV`. Automation uses `--backup-before-destroy` when desired and must always provide the exact
|
|
63
|
+
`--confirm-destroy` value. The data archive is retained, while Compose volumes, managed configuration
|
|
64
|
+
backups/files, markers and service state are removed. Copy the archive off the server before relying on it;
|
|
65
|
+
`--yes` cannot replace the exact confirmation.
|
|
66
|
+
|
|
67
|
+
## Updates and rollback
|
|
68
|
+
|
|
69
|
+
Changing the image/configuration/secret changes the service fingerprint. Before an update, Kitsune Kit backs up managed Compose/env files. It validates Compose, waits for health, reconciles firewall rules and only then records the new state. Failed updates restore previous files and restart the prior service where possible.
|
|
70
|
+
|
|
71
|
+
`kit rollback` restores captured managed configuration and preserves the volume. It does not promise database-level downgrade compatibility; test image upgrades and rollback in staging.
|
|
72
|
+
|
|
73
|
+
## External PostgreSQL
|
|
74
|
+
|
|
75
|
+
Set `enabled: true`, `mode: external`, `host` and `port` to represent a provider-managed database without
|
|
76
|
+
installing it on the VPS. `kit service postgres status` reports the endpoint; install, backup, remove and
|
|
77
|
+
destroy-data are intentionally unavailable. Kitsune Kit does not test provider TLS, create users/databases or
|
|
78
|
+
manage external backups. Supply those through the database provider and application deployment system.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Redis service
|
|
2
|
+
|
|
3
|
+
Redis is an optional Docker Compose service managed by Kitsune Kit on the target server. It is disabled, password-protected and private by default.
|
|
4
|
+
|
|
5
|
+
## Enable
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
services:
|
|
9
|
+
redis:
|
|
10
|
+
enabled: true
|
|
11
|
+
image: redis:7.2
|
|
12
|
+
publish: false
|
|
13
|
+
bind: 127.0.0.1
|
|
14
|
+
allowed_cidrs: []
|
|
15
|
+
port: 6379
|
|
16
|
+
password_env: REDIS_PASSWORD
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export REDIS_PASSWORD="$(ruby -rsecurerandom -e 'print SecureRandom.base64(36)')"
|
|
21
|
+
kit plan
|
|
22
|
+
kit service redis install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The generated service enables append-only persistence, requires the password and attaches to `kitsune-private`. Data lives in the Compose named `data` volume.
|
|
26
|
+
|
|
27
|
+
## Connectivity and publishing
|
|
28
|
+
|
|
29
|
+
With `publish: false`, only containers on the private Docker network can connect. If TCP access from outside the host is unavoidable, set `publish: true`, choose a bind address and provide narrow `allowed_cidrs`. Kitsune Kit installs matching `DOCKER-USER` rules and a final drop rule it owns.
|
|
30
|
+
|
|
31
|
+
Never treat the Redis password as an adequate substitute for network isolation. Validate port 6379 from an untrusted external network after any firewall/Docker change.
|
|
32
|
+
|
|
33
|
+
## Lifecycle
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
kit service redis status
|
|
37
|
+
kit service redis backup
|
|
38
|
+
kit service redis remove --yes
|
|
39
|
+
kit service redis destroy-data --backup-before-destroy --confirm-destroy redis@production
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `backup` pauses Redis, archives its volume remotely, then unpauses even if archiving fails.
|
|
43
|
+
- `remove` removes the container and owned firewall rules but preserves the data volume.
|
|
44
|
+
- `destroy-data` removes the volume, service files, backups and state after exact confirmation.
|
|
45
|
+
- `kit rollback` restores captured managed files/settings while preserving data.
|
|
46
|
+
|
|
47
|
+
Copy backups away from the Droplet, control access to them and regularly test restoration. Image downgrades may not understand data written by a newer Redis version; operational rollback and data-format compatibility are separate concerns.
|
|
48
|
+
|
|
49
|
+
## External Redis
|
|
50
|
+
|
|
51
|
+
Set `enabled: true`, `mode: external`, `host` and `port` for a provider-managed endpoint. Status is available,
|
|
52
|
+
but Kitsune Kit refuses install, backup, remove and destroy-data because those belong to the external provider. TLS,
|
|
53
|
+
availability and backup policy remain the operator/provider responsibility.
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Testing and quality gates
|
|
2
|
+
|
|
3
|
+
Kitsune Kit uses layers because no single test environment accurately represents provider APIs, SSH, systemd, UFW, Docker and public network exposure.
|
|
4
|
+
|
|
5
|
+
## Local commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bin/setup # install gems and run the local gate
|
|
9
|
+
bundle exec rake test # complete normal RSpec suite + coverage gate
|
|
10
|
+
bundle exec rake lint # RuboCop + ShellCheck
|
|
11
|
+
bundle exec rake security # Bundler Audit
|
|
12
|
+
bundle exec rake build # build the gem
|
|
13
|
+
bundle exec rake artifact_smoke # install the gem with fresh dependencies and verify its CLI
|
|
14
|
+
bundle exec rake ci # lint, tests and gem build
|
|
15
|
+
bundle exec rake integration # Docker-backed SSH/script suite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run an individual spec without applying whole-suite coverage thresholds:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle exec rspec spec/unit/configuration_spec.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Set `KITSUNE_COVERAGE_GATE=1` to force the full threshold for a custom selection or `0` for diagnostic subsets. CI always runs the complete gate.
|
|
25
|
+
|
|
26
|
+
## Test layers
|
|
27
|
+
|
|
28
|
+
### Unit/domain
|
|
29
|
+
|
|
30
|
+
Fast tests cover configuration precedence/validation, plans, errors/exit codes, redaction, reporters, state locking/writes, run journals, operation idempotence, rollback calculations, workflow resume/failure and TUI state/rendering. No network is used.
|
|
31
|
+
|
|
32
|
+
### Fakes and contracts
|
|
33
|
+
|
|
34
|
+
`FakeProvider`, `FakeTransport`, `FakeStateStore` and `FakeReporter` are deterministic memory adapters with
|
|
35
|
+
recorded calls or captured events. Shared examples define provider, transport and state-store behavior. Real
|
|
36
|
+
adapters execute those same contracts against simulated SDK/SSH boundaries, including timeouts, exact IDs,
|
|
37
|
+
status channels and hostile arguments.
|
|
38
|
+
|
|
39
|
+
### CLI process tests
|
|
40
|
+
|
|
41
|
+
The executable runs as a subprocess to verify global/help/version behavior, invalid syntax, exit codes, JSON parsing, non-TTY behavior and environment selection. Core behavior is separately tested through workflows so subprocess coverage is not mistaken for domain coverage.
|
|
42
|
+
|
|
43
|
+
### Shell scripts
|
|
44
|
+
|
|
45
|
+
Every remote script passes ShellCheck and `bash -n`. CI mounts scripts read-only into both supported Ubuntu images to ensure the distributed Bash parses there.
|
|
46
|
+
|
|
47
|
+
A simple container does not prove systemd/UFW behavior. The project does not claim otherwise; those effects are checked on a VPS in the E2E layer.
|
|
48
|
+
|
|
49
|
+
### Ephemeral SSH integration
|
|
50
|
+
|
|
51
|
+
`rake integration` builds an Ubuntu OpenSSH container with a temporary Ed25519 key. It verifies real public-key authentication, stdout/stderr/status separation, safe quoting, exact-byte upload/mode, host-key rejection, authentication failure and command timeout. It also uses a pseudo-terminal to prove that the TUI restores the main screen and console mode after normal exit and `SIGINT`. CI runs it on Ubuntu 22.04 and 24.04 base images.
|
|
52
|
+
|
|
53
|
+
Docker must be installed and running. If unavailable, examples report why they were skipped locally; the CI integration job first requires `docker info`, so it cannot silently pass without Docker.
|
|
54
|
+
|
|
55
|
+
### DigitalOcean E2E
|
|
56
|
+
|
|
57
|
+
The E2E suite is billable and never runs from a normal test command. Required variables:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
KITSUNE_E2E=1
|
|
61
|
+
DO_API_TOKEN
|
|
62
|
+
KITSUNE_E2E_SSH_KEY_ID
|
|
63
|
+
KITSUNE_E2E_KEY_PATH
|
|
64
|
+
POSTGRES_PASSWORD (generated by the test workflow)
|
|
65
|
+
REDIS_PASSWORD (generated by the test workflow)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use a dedicated DigitalOcean token and SSH key pair for this suite. The private key must not have a passphrase
|
|
69
|
+
because the GitHub runner cannot answer an unlock prompt. Generate a test-only Ed25519 pair locally:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ssh-keygen -t ed25519 -f ~/.ssh/kitsune_e2e_ed25519 -C kitsune-e2e -N ""
|
|
73
|
+
chmod 0600 ~/.ssh/kitsune_e2e_ed25519
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Upload only `~/.ssh/kitsune_e2e_ed25519.pub` in DigitalOcean under **Settings → Security → SSH keys → Add SSH
|
|
77
|
+
Key**. Never upload or paste the private file into DigitalOcean. `KITSUNE_E2E_SSH_KEY_ID` is the numeric ID
|
|
78
|
+
assigned to that uploaded public key, not its fingerprint or path. With `DO_API_TOKEN` already exported, list
|
|
79
|
+
the available IDs without printing the token:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
curl --fail --silent --show-error \
|
|
83
|
+
-H "Authorization: Bearer ${DO_API_TOKEN}" \
|
|
84
|
+
https://api.digitalocean.com/v2/account/keys |
|
|
85
|
+
ruby -rjson -e 'JSON.parse(STDIN.read).fetch("ssh_keys").each { |key| puts [key["id"], key["name"], key["fingerprint"]].join("\t") }'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The minimum custom token scopes used by this suite are `account:read`, `droplet:read`, `droplet:create`,
|
|
89
|
+
`droplet:delete`, `regions:read`, `sizes:read`, `actions:read`, `image:read`, `ssh_key:read`, `tag:read` and
|
|
90
|
+
`tag:create`. The dynamic expiry tag makes `tag:create` necessary. DNS scopes are not required by the current
|
|
91
|
+
E2E configuration. DigitalOcean does not allow changing an existing token's scopes; create a replacement token
|
|
92
|
+
if any permission is missing.
|
|
93
|
+
|
|
94
|
+
Before authorizing a billable run, confirm that the token can access the endpoint used by `doctor`:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl --output /dev/null --silent --show-error --write-out '%{http_code}\n' \
|
|
98
|
+
-H "Authorization: Bearer ${DO_API_TOKEN}" \
|
|
99
|
+
https://api.digitalocean.com/v2/account
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The expected status is `200`; `403` means the token lacks `account:read` or the selected team role does not grant
|
|
103
|
+
it.
|
|
104
|
+
|
|
105
|
+
For a local run, keep the token out of shell history and export the remaining values:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
printf 'DigitalOcean token: '
|
|
109
|
+
IFS= read -r -s DO_API_TOKEN
|
|
110
|
+
printf '\n'
|
|
111
|
+
export DO_API_TOKEN
|
|
112
|
+
export KITSUNE_E2E_SSH_KEY_ID="12345678"
|
|
113
|
+
export KITSUNE_E2E_KEY_PATH="$HOME/.ssh/kitsune_e2e_ed25519"
|
|
114
|
+
export KITSUNE_E2E=1
|
|
115
|
+
bundle exec rake e2e
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The optional `KITSUNE_E2E_REGION` and `KITSUNE_E2E_SIZE` variables override the defaults `sfo3` and
|
|
119
|
+
`s-1vcpu-1gb` when that combination is unavailable.
|
|
120
|
+
|
|
121
|
+
For GitHub Actions, add these repository secrets under **Settings → Secrets and variables → Actions**:
|
|
122
|
+
|
|
123
|
+
| GitHub secret | Value |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `DO_API_TOKEN` | Dedicated DigitalOcean token |
|
|
126
|
+
| `KITSUNE_E2E_SSH_KEY_ID` | Numeric ID of the uploaded public key |
|
|
127
|
+
| `KITSUNE_E2E_KEY` | Complete contents of the matching private key file |
|
|
128
|
+
|
|
129
|
+
GitHub writes `KITSUNE_E2E_KEY` to a restricted temporary file and sets `KITSUNE_E2E_KEY_PATH` itself. After
|
|
130
|
+
the workflow exists on GitHub, run **Actions → Real infrastructure → Run workflow**. It has no schedule and
|
|
131
|
+
therefore cannot create billable infrastructure unless someone dispatches it explicitly.
|
|
132
|
+
|
|
133
|
+
Run only in a dedicated account/project:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
KITSUNE_E2E=1 bundle exec rake e2e
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It creates a unique TTL-tagged Droplet, applies the complete configuration including PostgreSQL/Redis, runs CLI doctor/plan, demands a zero-change second plan, checks data ports from outside, rolls back and destroys in `ensure`.
|
|
140
|
+
|
|
141
|
+
Before creating anything, the suite derives the public half of `KITSUNE_E2E_KEY_PATH` and compares it with the
|
|
142
|
+
public key returned by DigitalOcean for `KITSUNE_E2E_SSH_KEY_ID`, then validates account API access. A key
|
|
143
|
+
mismatch or missing `account:read` permission fails without creating a billable Droplet. After creation, Kitsune Kit
|
|
144
|
+
waits up to two minutes for Ubuntu SSH readiness because an `active` API status and public IP do not guarantee
|
|
145
|
+
that `sshd` has finished starting.
|
|
146
|
+
|
|
147
|
+
The manually dispatched workflow serializes E2E runs and has a 45-minute limit. Its always-run cleanup job invokes
|
|
148
|
+
`script/cleanup-ci-resources`, which is dry-run by default and only considers `kitsune-ci` Droplets with an expired
|
|
149
|
+
`kitsune-expires-EPOCH` tag:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bundle exec ruby script/cleanup-ci-resources
|
|
153
|
+
bundle exec ruby script/cleanup-ci-resources --execute
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Unit tests inject a fake Droplet API into the cleanup class and prove that dry-run never deletes, untagged or
|
|
157
|
+
unexpired resources are ignored, and `--execute` uses only the filtered provider IDs.
|
|
158
|
+
|
|
159
|
+
## Coverage
|
|
160
|
+
|
|
161
|
+
SimpleCov records line and branch coverage. The gate is raised with the new core and must not be improved by excluding difficult production files. Destructive paths require explicit examples even when another path happens to cover the same line.
|
|
162
|
+
|
|
163
|
+
The complete suite enforces at least 80% line/50% branch coverage across all production code and 85% branch coverage in the domain core. For this gate, the core is `lib/kitsune/kit` excluding presentation (`cli.rb`, `tui/`, `reporters/`) and external adapters; operations/workflows/configuration/state remain included. `script/verify-core-coverage` calculates this directly from SimpleCov's current result.
|
|
164
|
+
|
|
165
|
+
Coverage is evidence of exercised control flow, not proof of provider/system correctness; contract, integration, security and E2E tests remain mandatory.
|
|
166
|
+
|
|
167
|
+
The dated manual source review, remediated findings and residual infrastructure risks are in
|
|
168
|
+
[Security audit](security-audit.md).
|
|
169
|
+
|
|
170
|
+
## CI jobs
|
|
171
|
+
|
|
172
|
+
Pull requests run:
|
|
173
|
+
|
|
174
|
+
- unit tests on every declared Ruby family;
|
|
175
|
+
- CLI process tests;
|
|
176
|
+
- adapter contracts;
|
|
177
|
+
- Ruby/shell lint;
|
|
178
|
+
- dependency audit;
|
|
179
|
+
- gem build, isolated installation and executable smoke test;
|
|
180
|
+
- Docker SSH/script integration for both supported Ubuntu releases.
|
|
181
|
+
|
|
182
|
+
The credential-gated, manually dispatched workflow runs the DigitalOcean lifecycle and cleanup. Secrets are
|
|
183
|
+
never available to untrusted pull-request jobs, and no calendar trigger creates billable infrastructure.
|
|
184
|
+
|
|
185
|
+
The artifact job deliberately resolves runtime dependencies without the repository lockfile, runs from outside
|
|
186
|
+
the checkout, verifies the exact public command set, and initializes a temporary project. This catches undeclared
|
|
187
|
+
standard-library gems and framework commands that a locked development bundle could hide.
|
|
188
|
+
|
|
189
|
+
## Writing a new operation test
|
|
190
|
+
|
|
191
|
+
At minimum cover:
|
|
192
|
+
|
|
193
|
+
1. absent resource plan/apply/verification;
|
|
194
|
+
2. already-correct no-change behavior;
|
|
195
|
+
3. allowed update and immutable/destructive drift;
|
|
196
|
+
4. failure before and after mutation;
|
|
197
|
+
5. resume after a persisted partial step;
|
|
198
|
+
6. rollback of owned state and refusal to touch pre-existing state;
|
|
199
|
+
7. timeout/cancellation;
|
|
200
|
+
8. hostile input and secret redaction.
|