--- - name: Create openclaw system user ansible.builtin.user: name: "{{ openclaw_user }}" comment: "OpenClaw Service User" system: true shell: /bin/bash create_home: true home: "{{ openclaw_home }}" state: present - name: Ensure openclaw home directory has correct ownership ansible.builtin.file: path: "{{ openclaw_home }}" owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" state: directory mode: '0755' - name: Configure .bashrc for openclaw user ansible.builtin.blockinfile: path: "{{ openclaw_home }}/.bashrc" marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw config" block: | # Enable 256 colors export TERM=xterm-256color export COLORTERM=truecolor # Add pnpm to PATH export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm" export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH" # Color support for common tools export CLICOLOR=1 export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43' # Aliases alias ls='ls --color=auto' alias grep='grep --color=auto' alias ll='ls -lah' create: true owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0644' - name: Add openclaw user to sudoers with scoped NOPASSWD ansible.builtin.copy: dest: "/etc/sudoers.d/{{ openclaw_user }}" mode: '0440' owner: root group: root content: | # OpenClaw sudo permissions (scoped for security) # # SECURITY NOTE: These permissions are intentionally limited. # If openclaw is compromised, attackers can only: # - Manage the openclaw service # - Run basic tailscale diagnostics # - View openclaw logs # # To grant full tailscale control (e.g., for self-healing VPN): # {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale * # # To grant full sudo (NOT RECOMMENDED): # {{ openclaw_user }} ALL=(ALL) NOPASSWD: ALL # Service control - openclaw service only {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start openclaw {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop openclaw {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart openclaw {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl status openclaw {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable openclaw {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable openclaw # daemon-reload affects all units (required after service file changes) {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload # Tailscale - diagnostics + connect/disconnect # NOTE: 'up' allows flags like --advertise-exit-node. For tighter control, # remove 'up' and 'down' lines - operator must then manage VPN manually. {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale status {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale up * {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale down {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ip * {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale version {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ping * {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale whois * # Journal access - openclaw logs only {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u openclaw * validate: /usr/sbin/visudo -cf %s - name: Set openclaw user as primary user for installation ansible.builtin.set_fact: openclaw_user: "{{ openclaw_user }}" openclaw_home: "{{ openclaw_home }}" - name: Create .bash_profile to source .bashrc for login shells ansible.builtin.copy: dest: "{{ openclaw_home }}/.bash_profile" owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0644' content: | # .bash_profile - Executed for login shells # Source .bashrc to ensure environment is loaded for login shells if [ -f ~/.bashrc ]; then . ~/.bashrc fi # Fix DBus issues for systemd user services - name: Get openclaw user ID ansible.builtin.command: "id -u {{ openclaw_user }}" register: openclaw_uid changed_when: false when: ansible_os_family == 'Debian' and not ci_test - name: Display openclaw user ID ansible.builtin.debug: msg: "OpenClaw user ID: {{ openclaw_uid.stdout }}" when: ansible_os_family == 'Debian' and not ci_test - name: Enable lingering for openclaw user (allows systemd user services without login) ansible.builtin.command: "loginctl enable-linger {{ openclaw_user }}" changed_when: false when: ansible_os_family == 'Debian' and not ci_test - name: Create runtime directory for openclaw user ansible.builtin.file: path: "/run/user/{{ openclaw_uid.stdout }}" state: directory owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0700' when: ansible_os_family == 'Debian' and not ci_test - name: Store openclaw UID as fact for later use ansible.builtin.set_fact: openclaw_uid_value: "{{ openclaw_uid.stdout }}" when: ansible_os_family == 'Debian' and not ci_test # SSH key configuration - name: Create .ssh directory for openclaw user ansible.builtin.file: path: "{{ openclaw_home }}/.ssh" state: directory owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0700' - name: Add SSH authorized keys for openclaw user ansible.posix.authorized_key: user: "{{ openclaw_user }}" state: present key: "{{ item }}" loop: "{{ openclaw_ssh_keys }}" when: openclaw_ssh_keys | length > 0 - name: Display SSH key configuration status ansible.builtin.debug: msg: "{{ openclaw_ssh_keys | length }} SSH key(s) configured for openclaw user" when: openclaw_ssh_keys | length > 0 - name: Display SSH key warning if none configured ansible.builtin.debug: msg: "No SSH keys configured. Set 'openclaw_ssh_keys' variable to allow SSH access." when: openclaw_ssh_keys | length == 0 - name: Set XDG_RUNTIME_DIR in .bashrc for openclaw user ansible.builtin.lineinfile: path: "{{ openclaw_home }}/.bashrc" line: 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' state: present create: true owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0644' when: ansible_os_family == 'Debian' and not ci_test - name: Set DBUS_SESSION_BUS_ADDRESS in .bashrc for openclaw user ansible.builtin.blockinfile: path: "{{ openclaw_home }}/.bashrc" marker: "# {mark} ANSIBLE MANAGED BLOCK - DBus config" block: | # DBus session bus configuration if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" fi create: true owner: "{{ openclaw_user }}" group: "{{ openclaw_user }}" mode: '0644' when: ansible_os_family == 'Debian' and not ci_test