--- # ============================================================================ # life-church/playbooks/thinkcenter-xrdp.yml # Install (if missing) + configure + enable xrdp on thinkcenter. Convergent: # safe to run whether xrdp is absent or already set up. thinkcenter ONLY. # CHANGES the system. become password from Infisical /lc-hosts at runtime. # # What it guarantees: # - xrdp + xorgxrdp installed (+ pipewire-module-xrdp so RDP has audio). # - the `xrdp` service user is in the `ssl-cert` group (classic xrdp gotcha: # without it xrdp can't read /etc/xrdp/key.pem and sessions fail). # - a per-user ~/.xsession selects Cinnamon for the RDP login. # - xrdp + xrdp-sesman enabled at boot AND started now. # - listening on 3389; UFW rule added only if UFW is active. # # UNDO: # sudo systemctl disable --now xrdp xrdp-sesman # sudo gpasswd -d xrdp ssl-cert # (only if you want to revert the group) # sudo apt remove --purge xrdp xorgxrdp # rm -f ~/.xsession # ============================================================================ # --- Play 1 (localhost): fetch thinkcenter sudo pass from Infisical ---------- - name: "thinkcenter xrdp — resolve become password from Infisical" hosts: localhost gather_facts: false tasks: - name: "[resolve] Infisical creds from env" ansible.builtin.set_fact: infisical_client_id: "{{ lookup('env','INFISICAL_CLIENT_ID') }}" infisical_client_secret: "{{ lookup('env','INFISICAL_CLIENT_SECRET') }}" infisical_project_id: "{{ lookup('env','INFISICAL_PROJECT_ID') }}" infisical_url: "{{ lookup('env','INFISICAL_API_URL') | default('http://192.168.122.41:8080', true) }}" infisical_env_slug: "{{ lookup('env','INFISICAL_ENV_SLUG') | default('prod', true) }}" - name: "[resolve] Assert Infisical creds present" ansible.builtin.assert: that: - infisical_client_id | length > 0 - infisical_client_secret | length > 0 - infisical_project_id | length > 0 fail_msg: "Infisical creds missing from Semaphore environment 'infisical-dev'." - name: "[infisical] Log in" infisical.vault.login: url: "{{ infisical_url }}" auth_method: universal_auth universal_auth_client_id: "{{ infisical_client_id }}" universal_auth_client_secret: "{{ infisical_client_secret }}" register: infisical_login no_log: true - name: "[infisical] Read /lc-hosts" infisical.vault.read_secrets: login_data: "{{ infisical_login.login_data }}" project_id: "{{ infisical_project_id }}" env_slug: "{{ infisical_env_slug }}" path: "/lc-hosts" as_dict: true register: lc_secrets no_log: true - name: "[set] Stash thinkcenter sudo pass + login user" ansible.builtin.set_fact: thinkcenter_become_pass: "{{ lc_secrets.secrets['THINKCENTER_SUDO_PASS'] }}" thinkcenter_login_user: "{{ lc_secrets.secrets['THINKCENTER_USER'] }}" no_log: true # --- Play 2: xrdp on thinkcenter ONLY ---------------------------------------- - name: "Install + configure + enable xrdp on thinkcenter" hosts: thinkcenter gather_facts: true become: true vars: ansible_become_pass: "{{ hostvars['localhost'].thinkcenter_become_pass }}" rdp_user: "{{ hostvars['localhost'].thinkcenter_login_user }}" xrdp_packages: - xrdp # the RDP server + sesman - xorgxrdp # Xorg backend for xrdp - pipewire-module-xrdp # audio redirection through PipeWire in RDP sessions xrdp_session_cmd: "cinnamon-session-cinnamon2d" # lightweight Cinnamon over RDP tasks: - name: "[safety] Confirm we are on thinkcenter" ansible.builtin.assert: that: inventory_hostname == 'thinkcenter' fail_msg: "Refusing to run — this play is scoped to thinkcenter only." - name: "[apt] Install xrdp packages (no-op if already present)" ansible.builtin.apt: name: "{{ xrdp_packages }}" state: present update_cache: true cache_valid_time: 3600 register: xrdp_pkg until: xrdp_pkg is succeeded retries: 3 delay: 15 - name: "[tls] Add xrdp service user to ssl-cert group (so it can read key.pem)" ansible.builtin.user: name: xrdp groups: ssl-cert append: true register: sslcert_grp - name: "[session] Set Cinnamon as the RDP session for {{ rdp_user }} (~/.xsession)" ansible.builtin.copy: dest: "/home/{{ rdp_user }}/.xsession" owner: "{{ rdp_user }}" group: "{{ rdp_user }}" mode: "0644" content: "{{ xrdp_session_cmd }}\n" register: xsession # Cinnamon uses D-Bus/PolicyKit; the common xrdp fix is a polkit rule so the # RDP session isn't blocked on colord authentication prompts. # Modern polkit (>=0.106, Mint 22.3 = polkit 124) uses JS rules in # /etc/polkit-1/rules.d/ — the old /etc/polkit-1/localauthority/*.pkla layout # no longer exists on this host. - name: "[polkit] Ensure rules.d directory exists" ansible.builtin.file: path: /etc/polkit-1/rules.d state: directory mode: "0750" - name: "[polkit] Allow colord actions in xrdp sessions (avoids auth popup loop)" ansible.builtin.copy: dest: /etc/polkit-1/rules.d/45-allow-colord.rules mode: "0644" content: | // Allow any user to manage color-manager devices/profiles. // Prevents the colord authentication popup loop in xrdp/Cinnamon sessions. polkit.addRule(function(action, subject) { if (action.id.indexOf("org.freedesktop.color-manager.") === 0) { return polkit.Result.YES; } }); register: polkit_rule - name: "[services] Enable + start xrdp and xrdp-sesman" ansible.builtin.systemd_service: name: "{{ item }}" enabled: true state: started loop: - xrdp - xrdp-sesman register: xrdp_services - name: "[services] Restart xrdp if config/group changed this run" ansible.builtin.systemd_service: name: xrdp state: restarted when: sslcert_grp.changed or xsession.changed or polkit_rule.changed or xrdp_pkg.changed - name: "[firewall] Is UFW active?" ansible.builtin.command: ufw status register: ufw_status changed_when: false failed_when: false - name: "[firewall] Allow RDP 3389/tcp when UFW is active" community.general.ufw: rule: allow port: "3389" proto: tcp when: "'Status: active' in (ufw_status.stdout | default(''))" - name: "[verify] xrdp listening on 3389?" ansible.builtin.shell: "ss -tlnp | grep -q ':3389' && echo listening || echo not-listening" register: rdp_listen changed_when: false - name: "[verify] Service states" ansible.builtin.shell: "systemctl is-active xrdp xrdp-sesman | tr '\n' ' '" register: svc_active changed_when: false - name: "[verify] Assert xrdp is up and listening" ansible.builtin.assert: that: - "'listening' in rdp_listen.stdout" - "'active' in svc_active.stdout" fail_msg: "xrdp is not listening on 3389 or a service is not active ({{ svc_active.stdout }})." success_msg: "xrdp is enabled, active, and listening on 3389." - name: "[report] xrdp summary" ansible.builtin.debug: msg: - "===== thinkcenter xrdp =====" - "packages changed this run : {{ xrdp_pkg.changed }}" - "xrdp added to ssl-cert : {{ sslcert_grp.changed }}" - ".xsession set for {{ rdp_user }} : {{ xsession.changed }}" - "colord polkit rule changed : {{ polkit_rule.changed }}" - "services (xrdp sesman) : {{ svc_active.stdout | trim }}" - "3389 listen : {{ rdp_listen.stdout }}" - "Connect via RDP to 100.96.78.26 (mesh) or 192.168.68.51 (LAN) as {{ rdp_user }}." - "NOTE: log out of the local console session first — xorgxrdp can't attach to an already-active seat0 login."