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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +112 -135
  5. data/SECURITY.md +25 -0
  6. data/bin/kit +4 -1
  7. data/docs/architecture/decisions/0001-product-boundary.md +23 -0
  8. data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
  9. data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
  10. data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
  11. data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
  12. data/docs/architecture/decisions/0006-tui-backend.md +21 -0
  13. data/docs/architecture/decisions/0007-compose-customization.md +30 -0
  14. data/docs/architecture/provider-adapters.md +49 -0
  15. data/docs/architecture.md +93 -0
  16. data/docs/commands.md +224 -0
  17. data/docs/configuration.md +226 -0
  18. data/docs/getting-started.md +133 -0
  19. data/docs/providers/digitalocean.md +52 -0
  20. data/docs/releasing.md +43 -0
  21. data/docs/roadmap.md +46 -0
  22. data/docs/security-audit.md +130 -0
  23. data/docs/security.md +116 -0
  24. data/docs/services/compose.md +164 -0
  25. data/docs/services/postgres.md +78 -0
  26. data/docs/services/redis.md +53 -0
  27. data/docs/testing.md +200 -0
  28. data/docs/troubleshooting.md +123 -0
  29. data/docs/tui.md +57 -0
  30. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  31. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  32. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  34. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  35. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  36. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  37. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  38. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  39. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  40. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  41. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  42. data/lib/kitsune/kit/application.rb +170 -0
  43. data/lib/kitsune/kit/cancellation.rb +21 -0
  44. data/lib/kitsune/kit/cli.rb +879 -64
  45. data/lib/kitsune/kit/clock.rb +11 -0
  46. data/lib/kitsune/kit/configuration.rb +577 -0
  47. data/lib/kitsune/kit/errors.rb +98 -0
  48. data/lib/kitsune/kit/events.rb +55 -0
  49. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  50. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  51. data/lib/kitsune/kit/operations/ensure_service.rb +302 -0
  52. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  53. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  54. data/lib/kitsune/kit/operations/service_files.rb +131 -0
  55. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  56. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  57. data/lib/kitsune/kit/plan.rb +72 -0
  58. data/lib/kitsune/kit/reporters/human.rb +89 -0
  59. data/lib/kitsune/kit/reporters/json.rb +65 -0
  60. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  61. data/lib/kitsune/kit/result.rb +28 -0
  62. data/lib/kitsune/kit/run_journal.rb +102 -0
  63. data/lib/kitsune/kit/run_logger.rb +37 -0
  64. data/lib/kitsune/kit/scripts/docker.sh +142 -0
  65. data/lib/kitsune/kit/scripts/firewall.sh +210 -0
  66. data/lib/kitsune/kit/scripts/metrics.sh +59 -0
  67. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  68. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  69. data/lib/kitsune/kit/scripts/unattended.sh +86 -0
  70. data/lib/kitsune/kit/scripts/user.sh +114 -0
  71. data/lib/kitsune/kit/secret_filter.rb +54 -0
  72. data/lib/kitsune/kit/secret_store.rb +32 -0
  73. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  74. data/lib/kitsune/kit/service_compose.rb +272 -0
  75. data/lib/kitsune/kit/state_store.rb +158 -0
  76. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  77. data/lib/kitsune/kit/tui/actions.rb +72 -0
  78. data/lib/kitsune/kit/tui/application.rb +35 -0
  79. data/lib/kitsune/kit/tui/controller.rb +162 -0
  80. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  81. data/lib/kitsune/kit/tui/state.rb +18 -0
  82. data/lib/kitsune/kit/tui/store.rb +88 -0
  83. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  84. data/lib/kitsune/kit/version.rb +1 -1
  85. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  86. data/lib/kitsune/kit/workflows/base.rb +31 -0
  87. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  88. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  89. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  90. data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
  91. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  92. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  93. data/lib/kitsune/kit/workflows/initialize_project.rb +129 -0
  94. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  95. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  96. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  97. data/lib/kitsune/kit.rb +42 -2
  98. metadata +125 -79
  99. data/.rspec +0 -3
  100. data/Rakefile +0 -8
  101. data/kitsune-kit-logo.jpg +0 -0
  102. data/lib/kitsune/blueprints/.env.template +0 -31
  103. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  104. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  105. data/lib/kitsune/blueprints/kit.env.template +0 -1
  106. data/lib/kitsune/kit/ansi_color.rb +0 -78
  107. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  108. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  109. data/lib/kitsune/kit/commands/dns.rb +0 -112
  110. data/lib/kitsune/kit/commands/init.rb +0 -148
  111. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  112. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  113. data/lib/kitsune/kit/commands/provision.rb +0 -43
  114. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  115. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  116. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  117. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  118. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  119. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  120. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  121. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  122. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  123. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  124. data/lib/kitsune/kit/defaults.rb +0 -91
  125. data/lib/kitsune/kit/env_loader.rb +0 -41
  126. data/lib/kitsune/kit/options_builder.rb +0 -26
  127. data/lib/kitsune/kit/provisioner.rb +0 -107
  128. 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.