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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -2
- data/CONTRIBUTING.md +83 -0
- data/README.md +108 -136
- data/SECURITY.md +25 -0
- data/bin/kit +4 -1
- data/docs/architecture/decisions/0001-product-boundary.md +23 -0
- data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
- data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
- data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
- data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
- data/docs/architecture/decisions/0006-tui-backend.md +21 -0
- data/docs/architecture/provider-adapters.md +49 -0
- data/docs/architecture.md +89 -0
- data/docs/commands.md +210 -0
- data/docs/configuration.md +210 -0
- data/docs/getting-started.md +133 -0
- data/docs/providers/digitalocean.md +52 -0
- data/docs/releasing.md +43 -0
- data/docs/roadmap.md +46 -0
- data/docs/security-audit.md +130 -0
- data/docs/security.md +116 -0
- data/docs/services/postgres.md +78 -0
- data/docs/services/redis.md +53 -0
- data/docs/testing.md +200 -0
- data/docs/troubleshooting.md +123 -0
- data/docs/tui.md +57 -0
- data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
- data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
- data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
- data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
- data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
- data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
- data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
- data/lib/kitsune/kit/adapters/provider.rb +34 -0
- data/lib/kitsune/kit/adapters/transport.rb +22 -0
- data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
- data/lib/kitsune/kit/application.rb +170 -0
- data/lib/kitsune/kit/cancellation.rb +21 -0
- data/lib/kitsune/kit/cli.rb +802 -64
- data/lib/kitsune/kit/clock.rb +11 -0
- data/lib/kitsune/kit/configuration.rb +493 -0
- data/lib/kitsune/kit/errors.rb +98 -0
- data/lib/kitsune/kit/events.rb +55 -0
- data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
- data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
- data/lib/kitsune/kit/operations/ensure_service.rb +285 -0
- data/lib/kitsune/kit/operations/remote_script.rb +214 -0
- data/lib/kitsune/kit/operations/service_backup.rb +71 -0
- data/lib/kitsune/kit/operations/service_files.rb +127 -0
- data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
- data/lib/kitsune/kit/operations/service_state.rb +59 -0
- data/lib/kitsune/kit/plan.rb +72 -0
- data/lib/kitsune/kit/reporters/human.rb +89 -0
- data/lib/kitsune/kit/reporters/json.rb +65 -0
- data/lib/kitsune/kit/reporters/reporter.rb +11 -0
- data/lib/kitsune/kit/result.rb +28 -0
- data/lib/kitsune/kit/run_journal.rb +102 -0
- data/lib/kitsune/kit/run_logger.rb +37 -0
- data/lib/kitsune/kit/scripts/docker.sh +137 -0
- data/lib/kitsune/kit/scripts/firewall.sh +205 -0
- data/lib/kitsune/kit/scripts/metrics.sh +54 -0
- data/lib/kitsune/kit/scripts/ssh.sh +95 -0
- data/lib/kitsune/kit/scripts/swap.sh +86 -0
- data/lib/kitsune/kit/scripts/unattended.sh +81 -0
- data/lib/kitsune/kit/scripts/user.sh +114 -0
- data/lib/kitsune/kit/secret_filter.rb +54 -0
- data/lib/kitsune/kit/secret_store.rb +32 -0
- data/lib/kitsune/kit/secret_stores/store.rb +12 -0
- data/lib/kitsune/kit/service_compose.rb +72 -0
- data/lib/kitsune/kit/state_store.rb +158 -0
- data/lib/kitsune/kit/state_stores/store.rb +15 -0
- data/lib/kitsune/kit/tui/actions.rb +72 -0
- data/lib/kitsune/kit/tui/application.rb +35 -0
- data/lib/kitsune/kit/tui/controller.rb +162 -0
- data/lib/kitsune/kit/tui/renderer.rb +134 -0
- data/lib/kitsune/kit/tui/state.rb +18 -0
- data/lib/kitsune/kit/tui/store.rb +88 -0
- data/lib/kitsune/kit/tui/terminal.rb +95 -0
- data/lib/kitsune/kit/version.rb +1 -1
- data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
- data/lib/kitsune/kit/workflows/base.rb +31 -0
- data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
- data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
- data/lib/kitsune/kit/workflows/doctor.rb +225 -0
- data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
- data/lib/kitsune/kit/workflows/import_server.rb +100 -0
- data/lib/kitsune/kit/workflows/initialize_project.rb +121 -0
- data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
- data/lib/kitsune/kit/workflows/rollback.rb +54 -0
- data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
- data/lib/kitsune/kit.rb +41 -2
- metadata +122 -79
- data/.rspec +0 -3
- data/Rakefile +0 -8
- data/kitsune-kit-logo.jpg +0 -0
- data/lib/kitsune/blueprints/.env.template +0 -31
- data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
- data/lib/kitsune/blueprints/docker/redis.yml +0 -23
- data/lib/kitsune/blueprints/kit.env.template +0 -1
- data/lib/kitsune/kit/ansi_color.rb +0 -78
- data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
- data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
- data/lib/kitsune/kit/commands/dns.rb +0 -112
- data/lib/kitsune/kit/commands/init.rb +0 -148
- data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
- data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
- data/lib/kitsune/kit/commands/provision.rb +0 -43
- data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
- data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
- data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
- data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
- data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
- data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
- data/lib/kitsune/kit/commands/setup_user.rb +0 -189
- data/lib/kitsune/kit/commands/ssh.rb +0 -46
- data/lib/kitsune/kit/commands/switch_env.rb +0 -42
- data/lib/kitsune/kit/defaults.rb +0 -91
- data/lib/kitsune/kit/env_loader.rb +0 -41
- data/lib/kitsune/kit/options_builder.rb +0 -26
- data/lib/kitsune/kit/provisioner.rb +0 -107
- data/sig/kitsune/kit.rbs +0 -6
|
@@ -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
|