From: Adam Kraitman Date: Mon, 19 Jan 2026 17:23:52 +0000 (+0200) Subject: teleport_ssh_node role for SSH node enrollment X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fheads%2Fwip-teleport-ssh-node;p=ceph-cm-ansible.git teleport_ssh_node role for SSH node enrollment This adds a new Ansible role that: - installs Teleport with pinned version - sets hostname and nodename consistently - generates teleport.yaml from templates - joins nodes using token + CA pin - enables and manages teleport.service - derives SSH labels from hostname automatically Designed for repeatable, safe enrollment of SSH nodes into Teleport. Signed-off-by: Adam Kraitman --- diff --git a/join-teleport.yml b/join-teleport.yml new file mode 100644 index 00000000..ba7e635e --- /dev/null +++ b/join-teleport.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + become: true + vars: + teleport_join_token: "{{ teleport_join_token | default(omit) }}" + + roles: + - teleport_ssh_node diff --git a/roles/teleport_ssh_node/README.md b/roles/teleport_ssh_node/README.md new file mode 100644 index 00000000..7e37f4d7 --- /dev/null +++ b/roles/teleport_ssh_node/README.md @@ -0,0 +1,170 @@ +Perfect — here is the **cleaned and corrected README.md** with exactly your requested changes applied: + +* ✅ Removed point **7 (air-gapped / bastion-based environments)** +* ✅ Removed the **Maintainers** section +* ✅ Replaced real hostnames with **generic examples** (`bob`, `mac`) +* ✅ Keeps GitHub-friendly rendering +* ✅ Keeps the hostname → label example inline (as you wanted) + +You can copy this **as-is** into `roles/teleport_ssh_node/README.md`: + +--- + +````md +# teleport_ssh_node + +Ansible role to **install, configure, and join a Linux host as a Teleport SSH node** +to an existing Teleport cluster. + +It supports: +- Ubuntu / Debian +- RHEL / CentOS / Rocky +- Teleport 15.x (version pinned) +- Idempotent re-runs +- Safe hostname handling +- Label-based node discovery + +--- + +## What this role does + +1. Sets the system hostname to the node FQDN +2. Installs Teleport **pinned to a specific version** +3. Creates `/etc/teleport.yaml` from template +4. Joins the node to the Teleport cluster using a join token +5. Enables and starts the teleport service +6. Registers the node with: + - Correct `nodename` + - `role` label derived from hostname + +--- + +## Requirements + +### On the control node +- Ansible 2.16+ +- SSH access to target nodes +- Valid Teleport join token +- Network access to the Teleport proxy + +### On the target node +- Systemd +- Python 3 +- Outbound access to the Teleport proxy + +--- + +## Role Variables + +### Required + +| Variable | Description | +|--------|------------| +| `teleport_join_token` | Teleport node join token | +| `teleport_ca_pin` | CA pin for secure joining | +| `teleport_proxy` | Proxy address (host:port) | + +--- + +### Optional + +| Variable | Default | Description | +|--------|---------|------------| +| `teleport_version` | `15.5.4` | Teleport version (pinned) | +| `teleport_config_path` | `/etc/teleport.yaml` | Config file path | +| `teleport_data_dir` | `/var/lib/teleport` | Data directory | + +--- + +## Hostname handling and labels + +The role automatically derives the Teleport node identity from the host itself. + +For a host with FQDN: + +```text +bob.example.com +```` + +The generated configuration will include: + +```yaml +teleport: + nodename: bob.example.com + +ssh_service: + labels: + role: bob +``` + +This ensures stable node identity, clean RBAC matching, and predictable resource +names in Teleport without requiring manual label management. + +--- + +## Example Playbook + +```yaml +- hosts: mac + become: true + vars_prompt: + - name: teleport_join_token + prompt: "Enter Teleport join token" + private: false + roles: + - teleport_ssh_node +``` + +--- + +## Example Run + +```bash +ansible-playbook join-teleport.yml \ + -i inventory \ + --limit bob.example.com +``` + +--- + +## Teleport configuration + +The role generates `/etc/teleport.yaml` with: + +* pinned Teleport version +* CA pin validation +* proxy configuration +* SSH service only (no auth/proxy) +* hostname-based nodename and labels +* safe defaults for production use + +--- + +## Supported Operating Systems + +| OS | Supported | +| --------------- | --------- | +| RHEL 8/9 | ✅ | +| Rocky 8/9 | ✅ | +| CentOS Stream 9 | ✅ | +| Ubuntu 20.04+ | ✅ | +| Debian 11+ | ✅ | + +--- + +## Idempotency + +The role is safe to run multiple times: + +* No duplicate joins +* No reinstallation if version is pinned +* Config updates trigger safe restart +* Service state enforced + +--- + +## License + +Apache-2.0 + +``` diff --git a/roles/teleport_ssh_node/defaults/main.yml b/roles/teleport_ssh_node/defaults/main.yml new file mode 100644 index 00000000..c69e13be --- /dev/null +++ b/roles/teleport_ssh_node/defaults/main.yml @@ -0,0 +1,12 @@ +--- +teleport_version: "15.5.4" +teleport_proxy: "teleport.ceph.com:443" +teleport_ca_pin: "sha256:3dcff35bc57ea8570af409ee9bad6a6c7acff5556bf7c094d34034e5f826b235" +teleport_join_token: "" + +teleport_pkg_version: "=15.5.4" +# Full node name used by Teleport +teleport_nodename: "{{ ansible_fqdn | default(ansible_hostname) }}" + +# Short role label (hostname without domain) +teleport_role_label: "{{ ansible_hostname }}" diff --git a/roles/teleport_ssh_node/handlers/main.yml b/roles/teleport_ssh_node/handlers/main.yml new file mode 100644 index 00000000..979e6f21 --- /dev/null +++ b/roles/teleport_ssh_node/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart teleport + systemd: + name: teleport + state: restarted diff --git a/roles/teleport_ssh_node/meta/main.yml b/roles/teleport_ssh_node/meta/main.yml new file mode 100644 index 00000000..313fd690 --- /dev/null +++ b/roles/teleport_ssh_node/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: secrets diff --git a/roles/teleport_ssh_node/tasks/config.yml b/roles/teleport_ssh_node/tasks/config.yml new file mode 100644 index 00000000..623477da --- /dev/null +++ b/roles/teleport_ssh_node/tasks/config.yml @@ -0,0 +1,28 @@ +--- +- name: Set hostname with domain + become: true + hostname: + name: "{{ ansible_hostname }}.front.sepia.ceph.com" + when: ansible_fqdn is not defined or not ansible_fqdn.endswith('front.sepia.ceph.com') + +- name: Assert join token provided + assert: + that: + - teleport_join_token is defined + - teleport_join_token | length > 0 + fail_msg: "teleport_join_token is required" + +- name: Render teleport config + template: + src: teleport.yaml.j2 + dest: /etc/teleport.yaml + owner: root + group: root + mode: '0644' + notify: restart teleport + +- name: Enable and start teleport + systemd: + name: teleport + enabled: true + state: started diff --git a/roles/teleport_ssh_node/tasks/facts.yml b/roles/teleport_ssh_node/tasks/facts.yml new file mode 100644 index 00000000..a3019799 --- /dev/null +++ b/roles/teleport_ssh_node/tasks/facts.yml @@ -0,0 +1,12 @@ +--- +- name: Assert FQDN exists + assert: + that: + - ansible_fqdn is defined + - ansible_fqdn | length > 0 + fail_msg: "ansible_fqdn is empty — fix hostname/DNS before continuing" + +- name: Set teleport facts + set_fact: + teleport_nodename: "{{ ansible_fqdn }}" + teleport_role_label: "{{ ansible_hostname }}" diff --git a/roles/teleport_ssh_node/tasks/hostname.yml b/roles/teleport_ssh_node/tasks/hostname.yml new file mode 100644 index 00000000..4360b0be --- /dev/null +++ b/roles/teleport_ssh_node/tasks/hostname.yml @@ -0,0 +1,4 @@ +--- +- name: Ensure hostname is FQDN + hostname: + name: "{{ ansible_fqdn }}" diff --git a/roles/teleport_ssh_node/tasks/install.yml b/roles/teleport_ssh_node/tasks/install.yml new file mode 100644 index 00000000..86ecc7b4 --- /dev/null +++ b/roles/teleport_ssh_node/tasks/install.yml @@ -0,0 +1,95 @@ +--- +# Install Teleport + +######################## +# Ubuntu / Debian +######################## + +- name: Install dependencies (Debian/Ubuntu) + ansible.builtin.apt: + name: curl + state: present + update_cache: yes + when: ansible_os_family == "Debian" + +- name: Add Teleport GPG key (Debian/Ubuntu) + ansible.builtin.get_url: + url: https://apt.releases.teleport.dev/gpg + dest: /usr/share/keyrings/teleport-archive-keyring.asc + mode: '0644' + when: ansible_os_family == "Debian" + +- name: Add Teleport apt repo (pinned major v15) + ansible.builtin.apt_repository: + repo: > + deb [signed-by=/usr/share/keyrings/teleport-archive-keyring.asc] + https://apt.releases.teleport.dev/ubuntu + {{ ansible_distribution_release }} stable/v15 + state: present + when: ansible_os_family == "Debian" + +- name: Install Teleport (pinned) on Debian/Ubuntu + ansible.builtin.apt: + name: "teleport{{ teleport_pkg_version }}" + state: present + update_cache: yes + when: ansible_os_family == "Debian" + + +######################## +# RHEL / CentOS / EL9 +######################## + +- name: Remove any old Teleport repo (EL) + ansible.builtin.file: + path: /etc/yum.repos.d/teleport.repo + state: absent + when: ansible_os_family == "RedHat" + +- name: Clean dnf metadata (EL) + ansible.builtin.command: dnf clean all + changed_when: false + when: ansible_os_family == "RedHat" + +- name: Install base deps safely (EL) + ansible.builtin.dnf: + name: + - curl + - dnf-plugins-core + state: present + disablerepo: "*" + enablerepo: "baseos,appstream" + when: ansible_os_family == "RedHat" + +- name: Install versionlock plugin (EL9) + ansible.builtin.dnf: + name: dnf-command(versionlock) + state: present + when: ansible_os_family == "RedHat" + +- name: Read OS ID (EL) + ansible.builtin.shell: ". /etc/os-release && echo $ID" + register: os_release_id + changed_when: false + when: ansible_os_family == "RedHat" + +- name: Add Teleport yum repo (pinned major v15) + ansible.builtin.command: > + dnf config-manager --add-repo + https://yum.releases.teleport.dev/{{ os_release_id.stdout | trim }}/{{ ansible_distribution_major_version }}/Teleport/{{ ansible_architecture }}/stable/v15/teleport.repo + args: + creates: /etc/yum.repos.d/teleport.repo + when: ansible_os_family == "RedHat" + +- name: Install Teleport (pinned) on EL + ansible.builtin.dnf: + name: "teleport-{{ teleport_version }}" + state: present + update_cache: true + when: ansible_os_family == "RedHat" + +- name: Lock Teleport version (EL) + ansible.builtin.command: dnf versionlock add teleport-{{ teleport_version }} + args: + creates: /etc/dnf/plugins/versionlock.list + when: ansible_os_family == "RedHat" diff --git a/roles/teleport_ssh_node/tasks/main.yml b/roles/teleport_ssh_node/tasks/main.yml new file mode 100644 index 00000000..1869ca13 --- /dev/null +++ b/roles/teleport_ssh_node/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- include_tasks: install.yml +- include_tasks: config.yml diff --git a/roles/teleport_ssh_node/templates/teleport.yaml.j2 b/roles/teleport_ssh_node/templates/teleport.yaml.j2 new file mode 100644 index 00000000..a13fe794 --- /dev/null +++ b/roles/teleport_ssh_node/templates/teleport.yaml.j2 @@ -0,0 +1,37 @@ +version: v3 + +teleport: + nodename: {{ teleport_nodename }} + data_dir: /var/lib/teleport + + join_params: + token_name: {{ teleport_join_token }} + method: token + + proxy_server: {{ teleport_proxy }} + ca_pin: {{ teleport_ca_pin }} + diag_addr: "" + + log: + output: stderr + severity: INFO + format: + output: text + +auth_service: + enabled: "no" + +ssh_service: + enabled: "yes" + labels: + role: {{ teleport_role_label }} + commands: + - name: hostname + command: [hostname] + period: 1m0s + +proxy_service: + enabled: "no" + https_keypairs: [] + https_keypairs_reload_interval: 0s + acme: {}