metis/pkg/plan/node_identity.go

238 lines
6.4 KiB
Go
Raw Normal View History

package plan
import (
"bytes"
"fmt"
"sort"
"strings"
"metis/pkg/config"
"metis/pkg/secrets"
)
func cloudInitUserData(cfg *config.NodeConfig, sec *secrets.NodeSecrets) string {
if cfg == nil {
return ""
}
if sec != nil && sec.CloudInit != "" {
return sec.CloudInit
}
var b bytes.Buffer
b.WriteString("#cloud-config\n")
b.WriteString(fmt.Sprintf("hostname: %s\n", cfg.Hostname))
if len(cfg.SSHKeys) > 0 {
b.WriteString("ssh_authorized_keys:\n")
for _, k := range cfg.SSHKeys {
b.WriteString(fmt.Sprintf(" - %s\n", k))
}
}
if hasNodePasswords(sec) {
b.WriteString("ssh_pwauth: true\n")
b.WriteString("disable_root: false\n")
}
b.WriteString("runcmd:\n")
b.WriteString(" - [bash, -lc, \"/usr/local/sbin/metis-apply-node-identity.sh\"]\n")
return b.String()
}
func firstbootEnvContent(cfg *config.NodeConfig, sec *secrets.NodeSecrets) string {
var b bytes.Buffer
b.WriteString(fmt.Sprintf("METIS_HOSTNAME=%s\n", shellQuote(cfg.Hostname)))
b.WriteString(fmt.Sprintf("METIS_SSH_USER=%s\n", shellQuote(cfg.SSHUser)))
b.WriteString("METIS_ATLAS_USER='atlas'\n")
b.WriteString(fmt.Sprintf("METIS_K3S_VERSION=%s\n", shellQuote(cfg.K3s.Version)))
if sec != nil {
if value := effectiveAtlasPassword(sec); value != "" {
b.WriteString(fmt.Sprintf("METIS_ATLAS_PASSWORD=%s\n", shellQuote(value)))
}
if value := strings.TrimSpace(sec.RootPassword); value != "" {
b.WriteString(fmt.Sprintf("METIS_ROOT_PASSWORD=%s\n", shellQuote(value)))
}
}
return b.String()
}
func cloudInitRootFSContent(sec *secrets.NodeSecrets) string {
var b bytes.Buffer
b.WriteString("#cloud-config\n")
if hasNodePasswords(sec) {
b.WriteString("ssh_pwauth: true\n")
b.WriteString("disable_root: false\n")
}
b.WriteString("runcmd:\n")
b.WriteString(" - [bash, -lc, \"/usr/local/sbin/metis-apply-node-identity.sh\"]\n")
return b.String()
}
func nodeIdentityScriptContent() string {
return `#!/usr/bin/env bash
set -euo pipefail
marker="/var/lib/metis/node-identity-applied.done"
env_file="/etc/metis/firstboot.env"
key_file="/etc/metis/authorized_keys"
sudoers_file="/etc/metis/sudoers-hecate"
default_groups=(adm sudo tty disk dialout audio video plugdev games users systemd-journal input render netdev)
if [ -f "${marker}" ]; then
exit 0
fi
mkdir -p /var/lib/metis
if [ -f "${env_file}" ]; then
# shellcheck disable=SC1090
. "${env_file}"
fi
atlas_user="${METIS_ATLAS_USER:-atlas}"
ssh_user="${METIS_SSH_USER:-${atlas_user}}"
atlas_password="${METIS_ATLAS_PASSWORD:-}"
root_password="${METIS_ROOT_PASSWORD:-}"
group_list=()
for group_name in "${default_groups[@]}"; do
if getent group "${group_name}" >/dev/null 2>&1; then
group_list+=("${group_name}")
fi
done
if [ "${#group_list[@]}" -gt 0 ]; then
group_csv="$(IFS=,; printf '%s' "${group_list[*]}")"
else
group_csv=""
fi
ensure_user() {
local user_name="$1"
[ -n "${user_name}" ] || return 0
if ! id "${user_name}" >/dev/null 2>&1; then
if [ -n "${group_csv}" ]; then
useradd -m -s /bin/bash -G "${group_csv}" "${user_name}"
else
useradd -m -s /bin/bash "${user_name}"
fi
elif [ -n "${group_csv}" ]; then
usermod -a -G "${group_csv}" "${user_name}" || true
fi
}
apply_password() {
local user_name="$1"
local plain_password="$2"
if ! id "${user_name}" >/dev/null 2>&1; then
return 0
fi
if [ -n "${plain_password}" ]; then
printf '%s:%s\n' "${user_name}" "${plain_password}" | chpasswd
passwd -u "${user_name}" >/dev/null 2>&1 || true
fi
}
install_keys() {
local user_name="$1"
[ -n "${user_name}" ] || return 0
[ -s "${key_file}" ] || return 0
local home_dir
home_dir="$(getent passwd "${user_name}" | cut -d: -f6)"
if [ -z "${home_dir}" ]; then
if [ "${user_name}" = "root" ]; then
home_dir="/root"
else
home_dir="/home/${user_name}"
fi
fi
install -d -m 700 "${home_dir}/.ssh"
install -m 600 "${key_file}" "${home_dir}/.ssh/authorized_keys"
chown -R "${user_name}:${user_name}" "${home_dir}/.ssh" 2>/dev/null || true
}
ensure_user "${atlas_user}"
if [ -n "${ssh_user}" ] && [ "${ssh_user}" != "root" ] && [ "${ssh_user}" != "${atlas_user}" ]; then
ensure_user "${ssh_user}"
fi
apply_password root "${root_password}"
apply_password "${atlas_user}" "${atlas_password}"
if [ -s "${key_file}" ]; then
install_keys root
install_keys "${atlas_user}"
if [ -n "${ssh_user}" ] && [ "${ssh_user}" != "root" ] && [ "${ssh_user}" != "${atlas_user}" ]; then
install_keys "${ssh_user}"
fi
fi
if [ -s "${sudoers_file}" ]; then
install -d -m 755 /etc/sudoers.d
install -m 440 "${sudoers_file}" /etc/sudoers.d/90-hecate-atlas
if command -v visudo >/dev/null 2>&1; then
visudo -cf /etc/sudoers.d/90-hecate-atlas >/dev/null 2>&1 || rm -f /etc/sudoers.d/90-hecate-atlas
fi
fi
systemctl restart ssh.service >/dev/null 2>&1 || systemctl restart sshd.service >/dev/null 2>&1 || systemctl restart ssh.socket >/dev/null 2>&1 || true
touch "${marker}"
`
}
func sshPasswordConfigContent(sec *secrets.NodeSecrets) string {
if !hasNodePasswords(sec) {
return ""
}
return "PasswordAuthentication yes\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nPermitRootLogin yes\nUsePAM yes\n"
}
func hasNodePasswords(sec *secrets.NodeSecrets) bool {
if sec == nil {
return false
}
return effectiveAtlasPassword(sec) != "" || strings.TrimSpace(sec.RootPassword) != ""
}
func effectiveAtlasPassword(sec *secrets.NodeSecrets) string {
if sec == nil {
return ""
}
return firstNonEmptyString(sec.AtlasPassword)
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func redactedSecretsForImage(sec *secrets.NodeSecrets) map[string]any {
if sec == nil {
return nil
}
debug := map[string]any{
"has_atlas_password": strings.TrimSpace(sec.AtlasPassword) != "",
"has_root_password": strings.TrimSpace(sec.RootPassword) != "",
"has_k3s_token": strings.TrimSpace(sec.K3sToken) != "",
"has_cloud_init_override": strings.TrimSpace(sec.CloudInit) != "",
}
if len(sec.Extra) > 0 {
keys := make([]string, 0, len(sec.Extra))
for key := range sec.Extra {
key = strings.TrimSpace(key)
if key == "" {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
debug["extra_keys"] = keys
}
return debug
}
func shellQuote(value string) string {
if value == "" {
return "''"
}
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}