kitsune-kit 0.4.0 → 0.5.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +108 -136
  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/provider-adapters.md +49 -0
  14. data/docs/architecture.md +89 -0
  15. data/docs/commands.md +210 -0
  16. data/docs/configuration.md +210 -0
  17. data/docs/getting-started.md +133 -0
  18. data/docs/providers/digitalocean.md +52 -0
  19. data/docs/releasing.md +43 -0
  20. data/docs/roadmap.md +46 -0
  21. data/docs/security-audit.md +130 -0
  22. data/docs/security.md +116 -0
  23. data/docs/services/postgres.md +78 -0
  24. data/docs/services/redis.md +53 -0
  25. data/docs/testing.md +200 -0
  26. data/docs/troubleshooting.md +123 -0
  27. data/docs/tui.md +57 -0
  28. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  29. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  30. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  31. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  32. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  34. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  35. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  36. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  37. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  38. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  39. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  40. data/lib/kitsune/kit/application.rb +170 -0
  41. data/lib/kitsune/kit/cancellation.rb +21 -0
  42. data/lib/kitsune/kit/cli.rb +802 -64
  43. data/lib/kitsune/kit/clock.rb +11 -0
  44. data/lib/kitsune/kit/configuration.rb +493 -0
  45. data/lib/kitsune/kit/errors.rb +98 -0
  46. data/lib/kitsune/kit/events.rb +55 -0
  47. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  48. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  49. data/lib/kitsune/kit/operations/ensure_service.rb +285 -0
  50. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  51. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  52. data/lib/kitsune/kit/operations/service_files.rb +127 -0
  53. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  54. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  55. data/lib/kitsune/kit/plan.rb +72 -0
  56. data/lib/kitsune/kit/reporters/human.rb +89 -0
  57. data/lib/kitsune/kit/reporters/json.rb +65 -0
  58. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  59. data/lib/kitsune/kit/result.rb +28 -0
  60. data/lib/kitsune/kit/run_journal.rb +102 -0
  61. data/lib/kitsune/kit/run_logger.rb +37 -0
  62. data/lib/kitsune/kit/scripts/docker.sh +137 -0
  63. data/lib/kitsune/kit/scripts/firewall.sh +205 -0
  64. data/lib/kitsune/kit/scripts/metrics.sh +54 -0
  65. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  66. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  67. data/lib/kitsune/kit/scripts/unattended.sh +81 -0
  68. data/lib/kitsune/kit/scripts/user.sh +114 -0
  69. data/lib/kitsune/kit/secret_filter.rb +54 -0
  70. data/lib/kitsune/kit/secret_store.rb +32 -0
  71. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  72. data/lib/kitsune/kit/service_compose.rb +72 -0
  73. data/lib/kitsune/kit/state_store.rb +158 -0
  74. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  75. data/lib/kitsune/kit/tui/actions.rb +72 -0
  76. data/lib/kitsune/kit/tui/application.rb +35 -0
  77. data/lib/kitsune/kit/tui/controller.rb +162 -0
  78. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  79. data/lib/kitsune/kit/tui/state.rb +18 -0
  80. data/lib/kitsune/kit/tui/store.rb +88 -0
  81. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  82. data/lib/kitsune/kit/version.rb +1 -1
  83. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  84. data/lib/kitsune/kit/workflows/base.rb +31 -0
  85. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  86. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  87. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  88. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  89. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  90. data/lib/kitsune/kit/workflows/initialize_project.rb +121 -0
  91. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  92. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  93. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  94. data/lib/kitsune/kit.rb +41 -2
  95. metadata +122 -79
  96. data/.rspec +0 -3
  97. data/Rakefile +0 -8
  98. data/kitsune-kit-logo.jpg +0 -0
  99. data/lib/kitsune/blueprints/.env.template +0 -31
  100. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  101. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  102. data/lib/kitsune/blueprints/kit.env.template +0 -1
  103. data/lib/kitsune/kit/ansi_color.rb +0 -78
  104. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  105. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  106. data/lib/kitsune/kit/commands/dns.rb +0 -112
  107. data/lib/kitsune/kit/commands/init.rb +0 -148
  108. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  109. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  110. data/lib/kitsune/kit/commands/provision.rb +0 -43
  111. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  112. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  113. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  114. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  115. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  116. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  117. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  118. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  119. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  120. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  121. data/lib/kitsune/kit/defaults.rb +0 -91
  122. data/lib/kitsune/kit/env_loader.rb +0 -41
  123. data/lib/kitsune/kit/options_builder.rb +0 -26
  124. data/lib/kitsune/kit/provisioner.rb +0 -107
  125. data/sig/kitsune/kit.rbs +0 -6
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ ssh_port="${2:?SSH port is required}"
6
+ shift 2
7
+ allowed_cidrs=("$@")
8
+ backup_dir="/var/lib/kitsune/backups/firewall"
9
+ added_file="${backup_dir}/rules-added"
10
+ pending_file="${backup_dir}/rules-pending"
11
+
12
+ validate() {
13
+ [[ "${ssh_port}" =~ ^[0-9]+$ ]] && (( ssh_port >= 1 && ssh_port <= 65535 ))
14
+ for cidr in "${allowed_cidrs[@]}"; do
15
+ [[ "${cidr}" =~ ^[0-9a-fA-F:.]+/[0-9]{1,3}$ ]] || {
16
+ echo "invalid CIDR: ${cidr}" >&2
17
+ exit 2
18
+ }
19
+ done
20
+ }
21
+
22
+ global_rule_exists() {
23
+ local port="$1"
24
+ ufw status | grep -Eq "^${port}/tcp[[:space:]]+ALLOW"
25
+ }
26
+
27
+ owned_global_rule_exists() {
28
+ local port="$1"
29
+ local comment="$2"
30
+ ufw status | grep -E "^${port}/tcp[[:space:]]+ALLOW" | grep -Fq "# ${comment}"
31
+ }
32
+
33
+ cidr_rule_exists() {
34
+ local port="$1"
35
+ local cidr="$2"
36
+ ufw status | grep -F "${port}/tcp" | grep -Fq "${cidr}"
37
+ }
38
+
39
+ owned_cidr_rule_exists() {
40
+ local port="$1"
41
+ local cidr="$2"
42
+ local comment="$3"
43
+ ufw status | grep -F "${port}/tcp" | grep -F "${cidr}" | grep -Fq "# ${comment}"
44
+ }
45
+
46
+ ensure_global_rule() {
47
+ local port="$1"
48
+ local comment="$2"
49
+ local next_file="$3"
50
+ local ownership="global|${port}"
51
+ if grep -Fxq "${ownership}" "${added_file}"; then
52
+ owned_global_rule_exists "${port}" "${comment}" || ufw allow "${port}/tcp" comment "${comment}"
53
+ printf '%s\n' "${ownership}" >> "${next_file}"
54
+ elif ! global_rule_exists "${port}"; then
55
+ ufw allow "${port}/tcp" comment "${comment}"
56
+ printf '%s\n' "${ownership}" >> "${next_file}"
57
+ fi
58
+ }
59
+
60
+ ensure_cidr_rule() {
61
+ local port="$1"
62
+ local cidr="$2"
63
+ local comment="$3"
64
+ local next_file="$4"
65
+ local ownership="cidr|${port}|${cidr}"
66
+ if grep -Fxq "${ownership}" "${added_file}"; then
67
+ owned_cidr_rule_exists "${port}" "${cidr}" "${comment}" || \
68
+ ufw allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}"
69
+ printf '%s\n' "${ownership}" >> "${next_file}"
70
+ elif ! cidr_rule_exists "${port}" "${cidr}"; then
71
+ ufw allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}"
72
+ printf '%s\n' "${ownership}" >> "${next_file}"
73
+ fi
74
+ }
75
+
76
+ delete_rule() {
77
+ local kind="$1"
78
+ local port="$2"
79
+ local cidr="${3:-}"
80
+ local comment="kitsune:ssh"
81
+ if [[ "${kind}" == "global" ]]; then
82
+ [[ "${port}" == "80" ]] && comment="kitsune:http"
83
+ [[ "${port}" == "443" ]] && comment="kitsune:https"
84
+ ufw --force delete allow "${port}/tcp" comment "${comment}" || true
85
+ else
86
+ ufw --force delete allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}" || true
87
+ fi
88
+ }
89
+
90
+ remove_stale_owned_rules() {
91
+ local next_file="$1"
92
+ local kind port cidr ownership
93
+ while IFS='|' read -r kind port cidr; do
94
+ [[ -z "${kind}" ]] && continue
95
+ ownership="${kind}|${port}"
96
+ [[ -n "${cidr}" ]] && ownership+="|${cidr}"
97
+ grep -Fxq "${ownership}" "${next_file}" || delete_rule "${kind}" "${port}" "${cidr}"
98
+ done < "${added_file}"
99
+ }
100
+
101
+ cleanup_new_rules() {
102
+ [[ -n "${next_file:-}" && -f "${next_file}" ]] || return
103
+ local kind port cidr ownership
104
+ while IFS='|' read -r kind port cidr; do
105
+ [[ -z "${kind}" ]] && continue
106
+ ownership="${kind}|${port}"
107
+ [[ -n "${cidr}" ]] && ownership+="|${cidr}"
108
+ grep -Fxq "${ownership}" "${added_file}" || delete_rule "${kind}" "${port}" "${cidr}"
109
+ done < "${next_file}"
110
+ rm -f "${next_file}"
111
+ }
112
+
113
+ apply() {
114
+ validate
115
+ install -d -m 0700 "${backup_dir}"
116
+ touch "${added_file}"
117
+ if [[ -f "${pending_file}" ]]; then
118
+ merged_file="$(mktemp "${backup_dir}/rules-owned.XXXXXX")"
119
+ sort -u "${added_file}" "${pending_file}" > "${merged_file}"
120
+ mv "${merged_file}" "${added_file}"
121
+ rm -f "${pending_file}"
122
+ fi
123
+ if [[ ! -f "${backup_dir}/snapshot-complete" ]]; then
124
+ if ufw status | grep -Fq "Status: active"; then
125
+ touch "${backup_dir}/active-before"
126
+ fi
127
+ touch "${backup_dir}/snapshot-complete"
128
+ fi
129
+ if ! dpkg-query -W -f='${Status}' ufw 2>/dev/null | grep -q "ok installed"; then
130
+ apt-get update
131
+ DEBIAN_FRONTEND=noninteractive apt-get install --yes ufw
132
+ touch "${backup_dir}/package-created"
133
+ fi
134
+
135
+ next_file="$(mktemp "${backup_dir}/rules-next.XXXXXX")"
136
+ trap cleanup_new_rules ERR INT TERM
137
+ if (( ${#allowed_cidrs[@]} == 0 )); then
138
+ ensure_global_rule "${ssh_port}" "kitsune:ssh" "${next_file}"
139
+ else
140
+ for cidr in "${allowed_cidrs[@]}"; do
141
+ ensure_cidr_rule "${ssh_port}" "${cidr}" "kitsune:ssh" "${next_file}"
142
+ done
143
+ fi
144
+ ensure_global_rule 80 "kitsune:http" "${next_file}"
145
+ ensure_global_rule 443 "kitsune:https" "${next_file}"
146
+ # Keep stale access until Kitsune Kit verifies a fresh SSH connection through
147
+ # the desired rules. The separate finalize action removes it afterward.
148
+ mv "${next_file}" "${pending_file}"
149
+ trap - ERR INT TERM
150
+ ufw --force enable
151
+ }
152
+
153
+ verify() {
154
+ validate
155
+ ufw status | grep -Fq "Status: active"
156
+ ufw status | grep -Eq '^80/tcp[[:space:]]+ALLOW'
157
+ ufw status | grep -Eq '^443/tcp[[:space:]]+ALLOW'
158
+ if (( ${#allowed_cidrs[@]} == 0 )); then
159
+ global_rule_exists "${ssh_port}"
160
+ else
161
+ for cidr in "${allowed_cidrs[@]}"; do
162
+ cidr_rule_exists "${ssh_port}" "${cidr}"
163
+ done
164
+ fi
165
+ }
166
+
167
+ finalize() {
168
+ validate
169
+ [[ -f "${pending_file}" ]] || {
170
+ echo "no pending firewall rule set to finalize" >&2
171
+ exit 4
172
+ }
173
+ remove_stale_owned_rules "${pending_file}"
174
+ mv "${pending_file}" "${added_file}"
175
+ }
176
+
177
+ verify_final() {
178
+ verify
179
+ [[ ! -f "${pending_file}" ]]
180
+ }
181
+
182
+ rollback() {
183
+ if [[ -f "${added_file}" || -f "${pending_file}" ]]; then
184
+ while IFS='|' read -r kind port cidr; do
185
+ [[ -z "${kind}" ]] && continue
186
+ delete_rule "${kind}" "${port}" "${cidr}"
187
+ done < <(cat "${added_file}" "${pending_file}" 2>/dev/null | sort -u)
188
+ fi
189
+ if [[ ! -f "${backup_dir}/active-before" ]]; then
190
+ ufw --force disable || true
191
+ fi
192
+ if [[ -f "${backup_dir}/package-created" ]]; then
193
+ apt-get remove --yes ufw || true
194
+ fi
195
+ rm -rf "${backup_dir}"
196
+ }
197
+
198
+ case "${action}" in
199
+ apply) apply ;;
200
+ verify) verify ;;
201
+ finalize) finalize ;;
202
+ verify_final) verify_final ;;
203
+ rollback) rollback ;;
204
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
205
+ esac
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ expected_sha256="${2:?installer SHA-256 is required}"
6
+ backup_dir="/var/lib/kitsune/backups/metrics"
7
+
8
+ validate() {
9
+ [[ "${expected_sha256}" =~ ^[a-f0-9]{64}$ ]] || {
10
+ echo "invalid installer SHA-256" >&2
11
+ exit 2
12
+ }
13
+ }
14
+
15
+ apply() {
16
+ validate
17
+ install -d -m 0700 "${backup_dir}"
18
+ if dpkg-query -W -f='${Status}' do-agent 2>/dev/null | grep -q "ok installed"; then
19
+ touch "${backup_dir}/package-existed"
20
+ return
21
+ fi
22
+ installer="$(mktemp)"
23
+ trap 'rm -f "${installer}"' EXIT
24
+ curl --fail --silent --show-error --location \
25
+ https://repos.insights.digitalocean.com/install.sh --output "${installer}"
26
+ test -s "${installer}"
27
+ printf '%s %s\n' "${expected_sha256}" "${installer}" | sha256sum --check --status
28
+ bash -n "${installer}"
29
+ bash "${installer}"
30
+ rm -f "${installer}"
31
+ trap - EXIT
32
+ touch "${backup_dir}/package-created"
33
+ }
34
+
35
+ verify() {
36
+ validate
37
+ dpkg-query -W -f='${Status}' do-agent 2>/dev/null | grep -q "ok installed"
38
+ systemctl is-active --quiet do-agent
39
+ }
40
+
41
+ rollback() {
42
+ if [[ -f "${backup_dir}/package-created" ]]; then
43
+ systemctl disable --now do-agent || true
44
+ apt-get remove --purge --yes do-agent || true
45
+ fi
46
+ rm -rf "${backup_dir}"
47
+ }
48
+
49
+ case "${action}" in
50
+ apply) apply ;;
51
+ verify) verify ;;
52
+ rollback) rollback ;;
53
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
54
+ esac
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ user="${2:?user is required}"
6
+ config_file="/etc/ssh/sshd_config.d/90-kitsune.conf"
7
+ backup_dir="/var/lib/kitsune/backups/ssh"
8
+ backup_file="${backup_dir}/90-kitsune.conf.before"
9
+
10
+ validate_user() {
11
+ [[ "${user}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || {
12
+ echo "invalid user name" >&2
13
+ exit 2
14
+ }
15
+ }
16
+
17
+ reload_ssh() {
18
+ sshd -t
19
+ systemctl reload ssh 2>/dev/null || systemctl reload sshd
20
+ }
21
+
22
+ write_policy() {
23
+ local root_policy="$1"
24
+ local temporary
25
+ temporary="$(mktemp)"
26
+ trap 'rm -f "${temporary}"' EXIT
27
+ cat > "${temporary}" <<EOF
28
+ # Managed by Kitsune Kit. Local changes will be replaced.
29
+ PubkeyAuthentication yes
30
+ PasswordAuthentication no
31
+ KbdInteractiveAuthentication no
32
+ PermitRootLogin ${root_policy}
33
+ AllowUsers ${user}
34
+ EOF
35
+ install -o root -g root -m 0644 "${temporary}" "${config_file}"
36
+ rm -f "${temporary}"
37
+ trap - EXIT
38
+ reload_ssh
39
+ }
40
+
41
+ apply() {
42
+ validate_user
43
+ install -d -m 0700 "${backup_dir}"
44
+ if [[ ! -f "${backup_dir}/snapshot-complete" ]]; then
45
+ if [[ -f "${config_file}" ]]; then
46
+ cp --preserve=mode,ownership "${config_file}" "${backup_file}"
47
+ else
48
+ touch "${backup_dir}/absent-before"
49
+ fi
50
+ touch "${backup_dir}/snapshot-complete"
51
+ fi
52
+
53
+ # Keep key-based root recovery until Kitsune Kit verifies a fresh deploy login.
54
+ write_policy "prohibit-password"
55
+ }
56
+
57
+ verify() {
58
+ validate_user
59
+ sshd -t
60
+ grep -Fxq "PasswordAuthentication no" "${config_file}"
61
+ grep -Fxq "PermitRootLogin prohibit-password" "${config_file}"
62
+ grep -Fxq "AllowUsers ${user}" "${config_file}"
63
+ }
64
+
65
+ finalize() {
66
+ validate_user
67
+ write_policy "no"
68
+ }
69
+
70
+ verify_final() {
71
+ validate_user
72
+ sshd -t
73
+ grep -Fxq "PasswordAuthentication no" "${config_file}"
74
+ grep -Fxq "PermitRootLogin no" "${config_file}"
75
+ grep -Fxq "AllowUsers ${user}" "${config_file}"
76
+ }
77
+
78
+ rollback() {
79
+ if [[ -f "${backup_file}" ]]; then
80
+ cp --preserve=mode,ownership "${backup_file}" "${config_file}"
81
+ elif [[ -f "${backup_dir}/absent-before" ]]; then
82
+ rm -f "${config_file}"
83
+ fi
84
+ reload_ssh
85
+ rm -rf "${backup_dir}"
86
+ }
87
+
88
+ case "${action}" in
89
+ apply) apply ;;
90
+ verify) verify ;;
91
+ finalize) finalize ;;
92
+ verify_final) verify_final ;;
93
+ rollback) rollback ;;
94
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
95
+ esac
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ size_gb="${2:?size is required}"
6
+ swappiness="${3:?swappiness is required}"
7
+ swap_file="/swapfile"
8
+ sysctl_file="/etc/sysctl.d/90-kitsune-swap.conf"
9
+ backup_dir="/var/lib/kitsune/backups/swap"
10
+ sysctl_backup="${backup_dir}/90-kitsune-swap.conf.before"
11
+
12
+ validate() {
13
+ [[ "${size_gb}" =~ ^[0-9]+$ ]] && (( size_gb >= 0 && size_gb <= 64 ))
14
+ [[ "${swappiness}" =~ ^[0-9]+$ ]] && (( swappiness >= 0 && swappiness <= 100 ))
15
+ }
16
+
17
+ apply() {
18
+ validate
19
+ install -d -m 0700 "${backup_dir}"
20
+ if [[ ! -f "${backup_dir}/sysctl-snapshot" ]]; then
21
+ if [[ -f "${sysctl_file}" ]]; then
22
+ cp --preserve=mode,ownership "${sysctl_file}" "${sysctl_backup}"
23
+ else
24
+ touch "${backup_dir}/sysctl-absent"
25
+ fi
26
+ touch "${backup_dir}/sysctl-snapshot"
27
+ fi
28
+ active=false
29
+ swapon --show=NAME --noheadings | grep -Fxq "${swap_file}" && active=true
30
+ if (( size_gb == 0 )) && [[ -f "${backup_dir}/swap-created" ]]; then
31
+ [[ "${active}" == true ]] && swapoff "${swap_file}"
32
+ rm -f "${swap_file}"
33
+ sed -i '\|/swapfile none swap sw 0 0|d' /etc/fstab
34
+ rm -f "${backup_dir}/current-size"
35
+ elif [[ "${active}" == true ]] && [[ ! -f "${backup_dir}/swap-created" ]]; then
36
+ touch "${backup_dir}/swap-existed"
37
+ elif (( size_gb > 0 )) && [[ -e "${swap_file}" && ! -f "${backup_dir}/swap-created" ]]; then
38
+ echo "refusing to overwrite unmanaged ${swap_file}" >&2
39
+ exit 3
40
+ elif (( size_gb > 0 )) && { [[ "${active}" == false ]] || \
41
+ [[ "$(cat "${backup_dir}/current-size" 2>/dev/null || true)" != "${size_gb}" ]]; }; then
42
+ [[ "${active}" == true ]] && swapoff "${swap_file}"
43
+ fallocate -l "${size_gb}G" "${swap_file}"
44
+ touch "${backup_dir}/swap-created"
45
+ chmod 0600 "${swap_file}"
46
+ mkswap "${swap_file}"
47
+ swapon "${swap_file}"
48
+ grep -Fq "${swap_file} none swap sw 0 0" /etc/fstab || printf '%s none swap sw 0 0\n' "${swap_file}" >> /etc/fstab
49
+ printf '%s\n' "${size_gb}" > "${backup_dir}/current-size"
50
+ fi
51
+ printf 'vm.swappiness=%s\n' "${swappiness}" > "${sysctl_file}"
52
+ sysctl --load "${sysctl_file}"
53
+ }
54
+
55
+ verify() {
56
+ validate
57
+ if (( size_gb > 0 )); then
58
+ swapon --show=NAME --noheadings | grep -Fxq "${swap_file}"
59
+ if [[ -f "${backup_dir}/swap-created" ]]; then
60
+ test "$(stat -c '%s' "${swap_file}")" -ge "$((size_gb * 1024 * 1024 * 1024))"
61
+ fi
62
+ fi
63
+ test "$(sysctl -n vm.swappiness)" = "${swappiness}"
64
+ }
65
+
66
+ rollback() {
67
+ if [[ -f "${backup_dir}/swap-created" ]]; then
68
+ swapoff "${swap_file}" || true
69
+ rm -f "${swap_file}"
70
+ sed -i '\|/swapfile none swap sw 0 0|d' /etc/fstab
71
+ fi
72
+ if [[ -f "${sysctl_backup}" ]]; then
73
+ cp --preserve=mode,ownership "${sysctl_backup}" "${sysctl_file}"
74
+ elif [[ -f "${backup_dir}/sysctl-absent" ]]; then
75
+ rm -f "${sysctl_file}"
76
+ fi
77
+ sysctl --system >/dev/null
78
+ rm -rf "${backup_dir}"
79
+ }
80
+
81
+ case "${action}" in
82
+ apply) apply ;;
83
+ verify) verify ;;
84
+ rollback) rollback ;;
85
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
86
+ esac
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ config_file="/etc/apt/apt.conf.d/20auto-upgrades"
6
+ backup_dir="/var/lib/kitsune/backups/unattended"
7
+ backup_file="${backup_dir}/20auto-upgrades.before"
8
+ packages=(unattended-upgrades apt-listchanges)
9
+
10
+ snapshot() {
11
+ [[ -f "${backup_dir}/snapshot-complete" ]] && return
12
+ : > "${backup_dir}/packages-before"
13
+ for package in "${packages[@]}"; do
14
+ if dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "ok installed"; then
15
+ printf '%s\n' "${package}" >> "${backup_dir}/packages-before"
16
+ fi
17
+ done
18
+ systemctl is-enabled --quiet unattended-upgrades.service && touch "${backup_dir}/enabled-before"
19
+ systemctl is-active --quiet unattended-upgrades.service && touch "${backup_dir}/active-before"
20
+ if [[ -f "${config_file}" ]]; then
21
+ cp --preserve=mode,ownership "${config_file}" "${backup_file}"
22
+ else
23
+ touch "${backup_dir}/config-absent"
24
+ fi
25
+ touch "${backup_dir}/snapshot-complete"
26
+ }
27
+
28
+ apply() {
29
+ install -d -m 0700 "${backup_dir}"
30
+ snapshot
31
+ apt-get update
32
+ DEBIAN_FRONTEND=noninteractive apt-get install --yes "${packages[@]}"
33
+ cat > "${config_file}" <<'EOF'
34
+ APT::Periodic::Update-Package-Lists "1";
35
+ APT::Periodic::Download-Upgradeable-Packages "1";
36
+ APT::Periodic::AutocleanInterval "7";
37
+ APT::Periodic::Unattended-Upgrade "1";
38
+ EOF
39
+ systemctl enable --now unattended-upgrades.service
40
+ }
41
+
42
+ verify() {
43
+ test -f "${config_file}"
44
+ systemctl is-enabled --quiet unattended-upgrades.service
45
+ systemctl is-active --quiet unattended-upgrades.service
46
+ }
47
+
48
+ restore_service_state() {
49
+ if [[ -f "${backup_dir}/enabled-before" ]]; then
50
+ systemctl enable unattended-upgrades.service || true
51
+ else
52
+ systemctl disable unattended-upgrades.service || true
53
+ fi
54
+ if [[ -f "${backup_dir}/active-before" ]]; then
55
+ systemctl start unattended-upgrades.service || true
56
+ else
57
+ systemctl stop unattended-upgrades.service || true
58
+ fi
59
+ }
60
+
61
+ rollback() {
62
+ if [[ -f "${backup_file}" ]]; then
63
+ cp --preserve=mode,ownership "${backup_file}" "${config_file}"
64
+ elif [[ -f "${backup_dir}/config-absent" ]]; then
65
+ rm -f "${config_file}"
66
+ fi
67
+ restore_service_state
68
+ for package in "${packages[@]}"; do
69
+ if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
70
+ apt-get remove --yes "${package}" || true
71
+ fi
72
+ done
73
+ rm -rf "${backup_dir}"
74
+ }
75
+
76
+ case "${action}" in
77
+ apply) apply ;;
78
+ verify) verify ;;
79
+ rollback) rollback ;;
80
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
81
+ esac
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ action="${1:?action is required}"
5
+ user="${2:?user is required}"
6
+ backup_dir="/var/lib/kitsune/backups/user-${user}"
7
+ sudoers_file="/etc/sudoers.d/${user}"
8
+ ssh_dir="/home/${user}/.ssh"
9
+ authorized_keys="${ssh_dir}/authorized_keys"
10
+
11
+ validate_user() {
12
+ [[ "${user}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || {
13
+ echo "invalid user name" >&2
14
+ exit 2
15
+ }
16
+ }
17
+
18
+ snapshot_existing_user() {
19
+ touch "${backup_dir}/existed-before"
20
+ id -nG "${user}" | tr ' ' '\n' | grep -Fxq sudo && touch "${backup_dir}/sudo-before"
21
+ if [[ -f "${sudoers_file}" ]]; then
22
+ cp --preserve=mode,ownership "${sudoers_file}" "${backup_dir}/sudoers.before"
23
+ else
24
+ touch "${backup_dir}/sudoers-absent"
25
+ fi
26
+ if [[ -d "${ssh_dir}" ]]; then
27
+ touch "${backup_dir}/ssh-dir-before"
28
+ stat -c '%a' "${ssh_dir}" > "${backup_dir}/ssh-dir-mode"
29
+ fi
30
+ if [[ -f "${authorized_keys}" ]]; then
31
+ cp --preserve=mode,ownership "${authorized_keys}" "${backup_dir}/authorized_keys.before"
32
+ else
33
+ touch "${backup_dir}/authorized-keys-absent"
34
+ fi
35
+ }
36
+
37
+ apply() {
38
+ validate_user
39
+ if [[ ! -s /root/.ssh/authorized_keys ]]; then
40
+ echo "root authorized_keys is missing or empty" >&2
41
+ exit 1
42
+ fi
43
+ install -d -m 0700 "${backup_dir}"
44
+ if [[ ! -f "${backup_dir}/snapshot-complete" ]]; then
45
+ if id "${user}" >/dev/null 2>&1; then
46
+ snapshot_existing_user
47
+ else
48
+ useradd --create-home --shell /bin/bash "${user}"
49
+ touch "${backup_dir}/created"
50
+ fi
51
+ touch "${backup_dir}/snapshot-complete"
52
+ fi
53
+
54
+ usermod -aG sudo "${user}"
55
+ temporary="$(mktemp)"
56
+ trap 'rm -f "${temporary}"' EXIT
57
+ printf '%s ALL=(ALL) NOPASSWD:ALL\n' "${user}" > "${temporary}"
58
+ chmod 0440 "${temporary}"
59
+ visudo -cf "${temporary}" >/dev/null
60
+ install -o root -g root -m 0440 "${temporary}" "${sudoers_file}"
61
+ rm -f "${temporary}"
62
+ trap - EXIT
63
+
64
+ install -d -o "${user}" -g "${user}" -m 0700 "${ssh_dir}"
65
+ install -o "${user}" -g "${user}" -m 0600 /root/.ssh/authorized_keys "${authorized_keys}"
66
+ }
67
+
68
+ verify() {
69
+ validate_user
70
+ id "${user}" >/dev/null
71
+ test -s "${authorized_keys}"
72
+ test "$(stat -c '%a' "${ssh_dir}")" = "700"
73
+ test "$(stat -c '%a' "${authorized_keys}")" = "600"
74
+ visudo -cf "${sudoers_file}" >/dev/null
75
+ }
76
+
77
+ restore_existing_user() {
78
+ if [[ ! -f "${backup_dir}/sudo-before" ]]; then
79
+ gpasswd -d "${user}" sudo || true
80
+ fi
81
+ if [[ -f "${backup_dir}/sudoers.before" ]]; then
82
+ cp --preserve=mode,ownership "${backup_dir}/sudoers.before" "${sudoers_file}"
83
+ elif [[ -f "${backup_dir}/sudoers-absent" ]]; then
84
+ rm -f "${sudoers_file}"
85
+ fi
86
+ if [[ -f "${backup_dir}/authorized_keys.before" ]]; then
87
+ cp --preserve=mode,ownership "${backup_dir}/authorized_keys.before" "${authorized_keys}"
88
+ elif [[ -f "${backup_dir}/authorized-keys-absent" ]]; then
89
+ rm -f "${authorized_keys}"
90
+ fi
91
+ if [[ -f "${backup_dir}/ssh-dir-before" ]]; then
92
+ chmod "$(< "${backup_dir}/ssh-dir-mode")" "${ssh_dir}"
93
+ else
94
+ rm -rf "${ssh_dir}"
95
+ fi
96
+ }
97
+
98
+ rollback() {
99
+ validate_user
100
+ if [[ -f "${backup_dir}/created" ]]; then
101
+ rm -f "${sudoers_file}"
102
+ userdel --remove "${user}" || true
103
+ elif [[ -f "${backup_dir}/existed-before" ]]; then
104
+ restore_existing_user
105
+ fi
106
+ rm -rf "${backup_dir}"
107
+ }
108
+
109
+ case "${action}" in
110
+ apply) apply ;;
111
+ verify) verify ;;
112
+ rollback) rollback ;;
113
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
114
+ esac
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Kitsune
6
+ module Kit
7
+ class SecretFilter
8
+ REDACTED = "[REDACTED]"
9
+ SENSITIVE_KEY = /(token|secret|password|private_key|authorization)/i
10
+ PRIVATE_KEY_BLOCK = /-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----/m
11
+
12
+ def initialize(values = [])
13
+ @values = []
14
+ values.each { |value| register(value) }
15
+ end
16
+
17
+ def register(value)
18
+ string = value.to_s
19
+ @values << string unless string.empty? || @values.include?(string)
20
+ self
21
+ end
22
+
23
+ def filter(value)
24
+ case value
25
+ when Hash
26
+ value.each_with_object({}) do |(key, item), result|
27
+ result[key] = key.to_s.match?(SENSITIVE_KEY) ? REDACTED : filter(item)
28
+ end
29
+ when Array
30
+ value.map { |item| filter(item) }
31
+ when String
32
+ filter_string(value)
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def filter_string(value)
41
+ filtered = @values.sort_by { |secret| -secret.length }.reduce(value.dup) do |text, secret|
42
+ text.gsub(secret, REDACTED)
43
+ end
44
+ redact_private_key(redact_uri_password(filtered))
45
+ end
46
+
47
+ def redact_uri_password(value)
48
+ value.gsub(%r{([a-z][a-z0-9+.-]*://[^:/@\s]+):[^@\s/]*@}i, "\\1:#{REDACTED}@")
49
+ end
50
+
51
+ def redact_private_key(value) = value.gsub(PRIVATE_KEY_BLOCK, REDACTED)
52
+ end
53
+ end
54
+ end