diff --git a/infrastructure/incus/README.md b/infrastructure/incus/README.md index 678dd52..2be0be5 100644 --- a/infrastructure/incus/README.md +++ b/infrastructure/incus/README.md @@ -117,15 +117,16 @@ Prod `192.168.10.12`(`ayatori-{dev,prod}.ad.ddupan.top`)。地址只在 NEC IX DHCP 池(`.128–.250`)之外。⚠ 镜像模板只在创建/复制时渲染 cloud-init seed, 对已有实例改地址不会生效(重启、`cloud-init clean` 都不行),只能重建实例。 -初始账号 `panxiao81`,使用 `ansible/files/panxiao81.pub` 公钥登录,可 sudo; -密码登录关闭。宿主也可使用 `incus exec local:ayatori-dev -- bash`。 +SSH 全部由 cloud-init 完成:安装 openssh-server,创建 `panxiao81`(`ansible/files/panxiao81.pub` +公钥、免密 sudo),装包后再写 `/etc/ssh/sshd_config.d/60-homelab.conf` 关闭密码与 root 登录。 +不使用 `ssh_pwauth`:它在装包前写出残缺的 `sshd_config`,包自带的 `UsePAM yes` 落不下来, +锁定密码的账号会被拒(2026-09-25 曾发生)。宿主也可使用 `incus exec local:ayatori-dev -- bash`。 provider 通过本地 Unix socket 操作,执行账号须有 socket 权限。 ```bash terraform -chdir=infrastructure/incus/terraform init terraform -chdir=infrastructure/incus/terraform plan -out=containers.tfplan terraform -chdir=infrastructure/incus/terraform apply -parallelism=1 containers.tfplan -ANSIBLE_LOCAL_TEMP=/tmp/ansible-incus ansible-playbook -i infrastructure/incus/ansible/inventory/hosts.yml infrastructure/incus/ansible/containers.yml terraform -chdir=infrastructure/incus/terraform output containers terraform -chdir=infrastructure/incus/terraform plan -detailed-exitcode ``` @@ -144,5 +145,3 @@ cloud-init 用户设置主要在首次启动执行,修改声明不能替代后 首次从同一远端并行创建实例时,Incus 7.5.1 曾出现 simplestreams 缓存目录 `mkdir ... file exists` 竞争;按上面的串行 apply 执行。失败后先重新 plan, 保留已成功创建的实例,不清理或销毁其资源。 - -`ansible/containers.yml` 通过本机 Incus exec 幂等安装并启用 SSH server。 diff --git a/infrastructure/incus/ansible/containers.yml b/infrastructure/incus/ansible/containers.yml deleted file mode 100644 index dc55d01..0000000 --- a/infrastructure/incus/ansible/containers.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: 准备 Ayatori 基础容器的 SSH 入口 - hosts: incus_hosts - gather_facts: false - tasks: - - name: 按容器协调 SSH - ansible.builtin.include_tasks: tasks/container-ssh.yml - loop: [ayatori-dev, ayatori-prod] - loop_control: - loop_var: incus_container diff --git a/infrastructure/incus/ansible/tasks/container-ssh.yml b/infrastructure/incus/ansible/tasks/container-ssh.yml deleted file mode 100644 index c1ebb64..0000000 --- a/infrastructure/incus/ansible/tasks/container-ssh.yml +++ /dev/null @@ -1,89 +0,0 @@ ---- -- name: 等待首次用户初始化 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, cloud-init, status, --wait] - changed_when: false - -- name: 检查 SSH server 是否安装 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, dpkg-query, -W, '-f=${Status}', openssh-server] - register: container_ssh_package - changed_when: false - failed_when: container_ssh_package.rc not in [0, 1] - -- name: 刷新容器包索引 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, apt-get, -o, Acquire::Retries=3, update] - when: "'install ok installed' not in container_ssh_package.stdout" - changed_when: true - register: container_apt_update - retries: 3 - delay: 5 - until: container_apt_update.rc == 0 - -- name: 安装 SSH server - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --env, DEBIAN_FRONTEND=noninteractive, --, apt-get, -o, Acquire::Retries=3, install, -y, --no-install-recommends, openssh-server] - when: "'install ok installed' not in container_ssh_package.stdout" - changed_when: true - register: container_apt_install - retries: 3 - delay: 5 - until: container_apt_install.rc == 0 - -# cloud-init 在未安装 sshd 时预先生成了只有 PasswordAuthentication 的配置; -# OpenSSH 默认 UsePAM=no 会拒绝锁定密码的公钥账号,显式采用 Ubuntu 的 PAM 模式。 -- name: 读取 SSH 配置 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, cat, /etc/ssh/sshd_config] - register: container_sshd_config - changed_when: false - -- name: 声明仅公钥 SSH 与 PAM 账号检查 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, tee, /etc/ssh/sshd_config] - stdin: "{{ container_sshd_desired }}" - vars: - container_sshd_desired: | - UsePAM yes - PubkeyAuthentication yes - PasswordAuthentication no - KbdInteractiveAuthentication no - PermitRootLogin no - Subsystem sftp internal-sftp - when: container_sshd_config.stdout | trim != container_sshd_desired | trim - register: container_sshd_write - changed_when: true - -# 24.04 新装时只有 ssh.socket 运行,/run/sshd 要等 ssh.service 的 RuntimeDirectory 才创建, -# 缺它时 sshd -t 直接失败;按同样的 0755 预建(/run 为 tmpfs,不留持久状态)。 -- name: 校验 SSH 配置 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, sh, -c, install -d -m 0755 /run/sshd && exec /usr/sbin/sshd -t] - changed_when: false - -- name: 更新运行中的 SSH 配置 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, systemctl, restart, ssh] - when: container_sshd_write is changed - changed_when: true - -- name: 检查 SSH service - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, systemctl, is-active, ssh] - register: container_ssh_active - changed_when: false - failed_when: container_ssh_active.rc not in [0, 3, 4] - -- name: 检查 SSH 自启 - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, systemctl, is-enabled, ssh] - register: container_ssh_enabled - changed_when: false - failed_when: container_ssh_enabled.rc not in [0, 1, 3, 4] - -- name: 启用 SSH - ansible.builtin.command: - argv: [incus, exec, "local:{{ incus_container }}", --, systemctl, enable, --now, ssh] - when: container_ssh_active.rc != 0 or container_ssh_enabled.stdout != 'enabled' - changed_when: true diff --git a/infrastructure/incus/terraform/main.tf b/infrastructure/incus/terraform/main.tf index 9981bf9..0021f2c 100644 --- a/infrastructure/incus/terraform/main.tf +++ b/infrastructure/incus/terraform/main.tf @@ -44,6 +44,37 @@ locals { } } +# 镜像不含 openssh-server。不设 ssh_pwauth:它会在装包前写出残缺的 sshd_config, +# 使包自带的默认配置(UsePAM yes、Include sshd_config.d)无法落地,锁定密码的账号随即被拒。 +# 改为装包后(defer)再写 drop-in,只覆盖需要收紧的项。 +locals { + ayatori_cloud_config = { + manage_etc_hosts = true + disable_root = true + users = [{ + name = "panxiao81" + groups = ["sudo"] + shell = "/bin/bash" + sudo = ["ALL=(ALL) NOPASSWD:ALL"] + lock_passwd = true + ssh_authorized_keys = [trimspace(file("${path.module}/../ansible/files/panxiao81.pub"))] + }] + # WAN 随机掉线,装包须重试。 + apt = { conf = "Acquire::Retries \"5\";" } + package_update = true + packages = ["openssh-server"] + write_files = [{ + path = "/etc/ssh/sshd_config.d/60-homelab.conf" + defer = true + content = <<-EOT + PasswordAuthentication no + KbdInteractiveAuthentication no + PermitRootLogin no + EOT + }] + } +} + resource "incus_instance" "ayatori" { for_each = toset(["dev", "prod"]) name = "ayatori-${each.key}" @@ -73,20 +104,9 @@ resource "incus_instance" "ayatori" { } } }) - "cloud-init.user-data" = "#cloud-config\n${yamlencode({ - hostname = "ayatori-${each.key}" - manage_etc_hosts = true - ssh_pwauth = false - disable_root = true - users = [{ - name = "panxiao81" - groups = ["sudo"] - shell = "/bin/bash" - sudo = ["ALL=(ALL) NOPASSWD:ALL"] - lock_passwd = true - ssh_authorized_keys = [trimspace(file("${path.module}/../ansible/files/panxiao81.pub"))] - }] - })}" + "cloud-init.user-data" = "#cloud-config\n${yamlencode(merge(local.ayatori_cloud_config, { + hostname = "ayatori-${each.key}" + }))}" } device { name = "root"