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
@@ -0,0 +1,210 @@
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
+ apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
12
+
13
+ apt_get() {
14
+ DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
15
+ }
16
+
17
+ validate() {
18
+ [[ "${ssh_port}" =~ ^[0-9]+$ ]] && (( ssh_port >= 1 && ssh_port <= 65535 ))
19
+ for cidr in "${allowed_cidrs[@]}"; do
20
+ [[ "${cidr}" =~ ^[0-9a-fA-F:.]+/[0-9]{1,3}$ ]] || {
21
+ echo "invalid CIDR: ${cidr}" >&2
22
+ exit 2
23
+ }
24
+ done
25
+ }
26
+
27
+ global_rule_exists() {
28
+ local port="$1"
29
+ ufw status | grep -Eq "^${port}/tcp[[:space:]]+ALLOW"
30
+ }
31
+
32
+ owned_global_rule_exists() {
33
+ local port="$1"
34
+ local comment="$2"
35
+ ufw status | grep -E "^${port}/tcp[[:space:]]+ALLOW" | grep -Fq "# ${comment}"
36
+ }
37
+
38
+ cidr_rule_exists() {
39
+ local port="$1"
40
+ local cidr="$2"
41
+ ufw status | grep -F "${port}/tcp" | grep -Fq "${cidr}"
42
+ }
43
+
44
+ owned_cidr_rule_exists() {
45
+ local port="$1"
46
+ local cidr="$2"
47
+ local comment="$3"
48
+ ufw status | grep -F "${port}/tcp" | grep -F "${cidr}" | grep -Fq "# ${comment}"
49
+ }
50
+
51
+ ensure_global_rule() {
52
+ local port="$1"
53
+ local comment="$2"
54
+ local next_file="$3"
55
+ local ownership="global|${port}"
56
+ if grep -Fxq "${ownership}" "${added_file}"; then
57
+ owned_global_rule_exists "${port}" "${comment}" || ufw allow "${port}/tcp" comment "${comment}"
58
+ printf '%s\n' "${ownership}" >> "${next_file}"
59
+ elif ! global_rule_exists "${port}"; then
60
+ ufw allow "${port}/tcp" comment "${comment}"
61
+ printf '%s\n' "${ownership}" >> "${next_file}"
62
+ fi
63
+ }
64
+
65
+ ensure_cidr_rule() {
66
+ local port="$1"
67
+ local cidr="$2"
68
+ local comment="$3"
69
+ local next_file="$4"
70
+ local ownership="cidr|${port}|${cidr}"
71
+ if grep -Fxq "${ownership}" "${added_file}"; then
72
+ owned_cidr_rule_exists "${port}" "${cidr}" "${comment}" || \
73
+ ufw allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}"
74
+ printf '%s\n' "${ownership}" >> "${next_file}"
75
+ elif ! cidr_rule_exists "${port}" "${cidr}"; then
76
+ ufw allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}"
77
+ printf '%s\n' "${ownership}" >> "${next_file}"
78
+ fi
79
+ }
80
+
81
+ delete_rule() {
82
+ local kind="$1"
83
+ local port="$2"
84
+ local cidr="${3:-}"
85
+ local comment="kitsune:ssh"
86
+ if [[ "${kind}" == "global" ]]; then
87
+ [[ "${port}" == "80" ]] && comment="kitsune:http"
88
+ [[ "${port}" == "443" ]] && comment="kitsune:https"
89
+ ufw --force delete allow "${port}/tcp" comment "${comment}" || true
90
+ else
91
+ ufw --force delete allow from "${cidr}" to any port "${port}" proto tcp comment "${comment}" || true
92
+ fi
93
+ }
94
+
95
+ remove_stale_owned_rules() {
96
+ local next_file="$1"
97
+ local kind port cidr ownership
98
+ while IFS='|' read -r kind port cidr; do
99
+ [[ -z "${kind}" ]] && continue
100
+ ownership="${kind}|${port}"
101
+ [[ -n "${cidr}" ]] && ownership+="|${cidr}"
102
+ grep -Fxq "${ownership}" "${next_file}" || delete_rule "${kind}" "${port}" "${cidr}"
103
+ done < "${added_file}"
104
+ }
105
+
106
+ cleanup_new_rules() {
107
+ [[ -n "${next_file:-}" && -f "${next_file}" ]] || return
108
+ local kind port cidr ownership
109
+ while IFS='|' read -r kind port cidr; do
110
+ [[ -z "${kind}" ]] && continue
111
+ ownership="${kind}|${port}"
112
+ [[ -n "${cidr}" ]] && ownership+="|${cidr}"
113
+ grep -Fxq "${ownership}" "${added_file}" || delete_rule "${kind}" "${port}" "${cidr}"
114
+ done < "${next_file}"
115
+ rm -f "${next_file}"
116
+ }
117
+
118
+ apply() {
119
+ validate
120
+ install -d -m 0700 "${backup_dir}"
121
+ touch "${added_file}"
122
+ if [[ -f "${pending_file}" ]]; then
123
+ merged_file="$(mktemp "${backup_dir}/rules-owned.XXXXXX")"
124
+ sort -u "${added_file}" "${pending_file}" > "${merged_file}"
125
+ mv "${merged_file}" "${added_file}"
126
+ rm -f "${pending_file}"
127
+ fi
128
+ if [[ ! -f "${backup_dir}/snapshot-complete" ]]; then
129
+ if ufw status | grep -Fq "Status: active"; then
130
+ touch "${backup_dir}/active-before"
131
+ fi
132
+ touch "${backup_dir}/snapshot-complete"
133
+ fi
134
+ if ! dpkg-query -W -f='${Status}' ufw 2>/dev/null | grep -q "ok installed"; then
135
+ apt_get update
136
+ apt_get install --yes ufw
137
+ touch "${backup_dir}/package-created"
138
+ fi
139
+
140
+ next_file="$(mktemp "${backup_dir}/rules-next.XXXXXX")"
141
+ trap cleanup_new_rules ERR INT TERM
142
+ if (( ${#allowed_cidrs[@]} == 0 )); then
143
+ ensure_global_rule "${ssh_port}" "kitsune:ssh" "${next_file}"
144
+ else
145
+ for cidr in "${allowed_cidrs[@]}"; do
146
+ ensure_cidr_rule "${ssh_port}" "${cidr}" "kitsune:ssh" "${next_file}"
147
+ done
148
+ fi
149
+ ensure_global_rule 80 "kitsune:http" "${next_file}"
150
+ ensure_global_rule 443 "kitsune:https" "${next_file}"
151
+ # Keep stale access until Kitsune Kit verifies a fresh SSH connection through
152
+ # the desired rules. The separate finalize action removes it afterward.
153
+ mv "${next_file}" "${pending_file}"
154
+ trap - ERR INT TERM
155
+ ufw --force enable
156
+ }
157
+
158
+ verify() {
159
+ validate
160
+ ufw status | grep -Fq "Status: active"
161
+ ufw status | grep -Eq '^80/tcp[[:space:]]+ALLOW'
162
+ ufw status | grep -Eq '^443/tcp[[:space:]]+ALLOW'
163
+ if (( ${#allowed_cidrs[@]} == 0 )); then
164
+ global_rule_exists "${ssh_port}"
165
+ else
166
+ for cidr in "${allowed_cidrs[@]}"; do
167
+ cidr_rule_exists "${ssh_port}" "${cidr}"
168
+ done
169
+ fi
170
+ }
171
+
172
+ finalize() {
173
+ validate
174
+ [[ -f "${pending_file}" ]] || {
175
+ echo "no pending firewall rule set to finalize" >&2
176
+ exit 4
177
+ }
178
+ remove_stale_owned_rules "${pending_file}"
179
+ mv "${pending_file}" "${added_file}"
180
+ }
181
+
182
+ verify_final() {
183
+ verify
184
+ [[ ! -f "${pending_file}" ]]
185
+ }
186
+
187
+ rollback() {
188
+ if [[ -f "${added_file}" || -f "${pending_file}" ]]; then
189
+ while IFS='|' read -r kind port cidr; do
190
+ [[ -z "${kind}" ]] && continue
191
+ delete_rule "${kind}" "${port}" "${cidr}"
192
+ done < <(cat "${added_file}" "${pending_file}" 2>/dev/null | sort -u)
193
+ fi
194
+ if [[ ! -f "${backup_dir}/active-before" ]]; then
195
+ ufw --force disable || true
196
+ fi
197
+ if [[ -f "${backup_dir}/package-created" ]]; then
198
+ apt_get remove --yes ufw || true
199
+ fi
200
+ rm -rf "${backup_dir}"
201
+ }
202
+
203
+ case "${action}" in
204
+ apply) apply ;;
205
+ verify) verify ;;
206
+ finalize) finalize ;;
207
+ verify_final) verify_final ;;
208
+ rollback) rollback ;;
209
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
210
+ esac
@@ -0,0 +1,59 @@
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
+ apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
8
+
9
+ apt_get() {
10
+ DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
11
+ }
12
+
13
+ validate() {
14
+ [[ "${expected_sha256}" =~ ^[a-f0-9]{64}$ ]] || {
15
+ echo "invalid installer SHA-256" >&2
16
+ exit 2
17
+ }
18
+ }
19
+
20
+ apply() {
21
+ validate
22
+ install -d -m 0700 "${backup_dir}"
23
+ if dpkg-query -W -f='${Status}' do-agent 2>/dev/null | grep -q "ok installed"; then
24
+ touch "${backup_dir}/package-existed"
25
+ return
26
+ fi
27
+ installer="$(mktemp)"
28
+ trap 'rm -f "${installer}"' EXIT
29
+ curl --fail --silent --show-error --location \
30
+ https://repos.insights.digitalocean.com/install.sh --output "${installer}"
31
+ test -s "${installer}"
32
+ printf '%s %s\n' "${expected_sha256}" "${installer}" | sha256sum --check --status
33
+ bash -n "${installer}"
34
+ bash "${installer}"
35
+ rm -f "${installer}"
36
+ trap - EXIT
37
+ touch "${backup_dir}/package-created"
38
+ }
39
+
40
+ verify() {
41
+ validate
42
+ dpkg-query -W -f='${Status}' do-agent 2>/dev/null | grep -q "ok installed"
43
+ systemctl is-active --quiet do-agent
44
+ }
45
+
46
+ rollback() {
47
+ if [[ -f "${backup_dir}/package-created" ]]; then
48
+ systemctl disable --now do-agent || true
49
+ apt_get remove --purge --yes do-agent || true
50
+ fi
51
+ rm -rf "${backup_dir}"
52
+ }
53
+
54
+ case "${action}" in
55
+ apply) apply ;;
56
+ verify) verify ;;
57
+ rollback) rollback ;;
58
+ *) echo "unknown action: ${action}" >&2; exit 2 ;;
59
+ 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,86 @@
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
+ apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
10
+
11
+ apt_get() {
12
+ DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
13
+ }
14
+
15
+ snapshot() {
16
+ [[ -f "${backup_dir}/snapshot-complete" ]] && return
17
+ : > "${backup_dir}/packages-before"
18
+ for package in "${packages[@]}"; do
19
+ if dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "ok installed"; then
20
+ printf '%s\n' "${package}" >> "${backup_dir}/packages-before"
21
+ fi
22
+ done
23
+ systemctl is-enabled --quiet unattended-upgrades.service && touch "${backup_dir}/enabled-before"
24
+ systemctl is-active --quiet unattended-upgrades.service && touch "${backup_dir}/active-before"
25
+ if [[ -f "${config_file}" ]]; then
26
+ cp --preserve=mode,ownership "${config_file}" "${backup_file}"
27
+ else
28
+ touch "${backup_dir}/config-absent"
29
+ fi
30
+ touch "${backup_dir}/snapshot-complete"
31
+ }
32
+
33
+ apply() {
34
+ install -d -m 0700 "${backup_dir}"
35
+ snapshot
36
+ apt_get update
37
+ apt_get install --yes "${packages[@]}"
38
+ cat > "${config_file}" <<'EOF'
39
+ APT::Periodic::Update-Package-Lists "1";
40
+ APT::Periodic::Download-Upgradeable-Packages "1";
41
+ APT::Periodic::AutocleanInterval "7";
42
+ APT::Periodic::Unattended-Upgrade "1";
43
+ EOF
44
+ systemctl enable --now unattended-upgrades.service
45
+ }
46
+
47
+ verify() {
48
+ test -f "${config_file}"
49
+ systemctl is-enabled --quiet unattended-upgrades.service
50
+ systemctl is-active --quiet unattended-upgrades.service
51
+ }
52
+
53
+ restore_service_state() {
54
+ if [[ -f "${backup_dir}/enabled-before" ]]; then
55
+ systemctl enable unattended-upgrades.service || true
56
+ else
57
+ systemctl disable unattended-upgrades.service || true
58
+ fi
59
+ if [[ -f "${backup_dir}/active-before" ]]; then
60
+ systemctl start unattended-upgrades.service || true
61
+ else
62
+ systemctl stop unattended-upgrades.service || true
63
+ fi
64
+ }
65
+
66
+ rollback() {
67
+ if [[ -f "${backup_file}" ]]; then
68
+ cp --preserve=mode,ownership "${backup_file}" "${config_file}"
69
+ elif [[ -f "${backup_dir}/config-absent" ]]; then
70
+ rm -f "${config_file}"
71
+ fi
72
+ restore_service_state
73
+ for package in "${packages[@]}"; do
74
+ if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
75
+ apt_get remove --yes "${package}" || true
76
+ fi
77
+ done
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,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