--- # Deploy OpenClaw AI Gateway on a Proxmox VM # # OpenClaw: https://docs.openclaw.ai # Ansible install docs: https://docs.openclaw.ai/install/ansible # Signal channel docs: https://docs.openclaw.ai/channels/signal # # Prerequisites: # Inventory host: openclaw.toal.ca (in group 'openclaw') # host_vars required: # openclaw_vm_ssh_public_key — SSH public key injected via cloud-init # openclaw_vm_ip — static IP or 'dhcp' # openclaw_vm_gateway — required for static IP # openclaw_vm_vnet — Proxmox SDN VNet (e.g. lan) # # Vault secrets (1Password): # vault_proxmox_token_secret — Proxmox API token # vault_openclaw_api_key — Model provider API key (Anthropic, OpenAI, etc.) # vault_openclaw_signal_phone — Signal account phone number (E.164, if Signal enabled) # # Security architecture: # - OPNsense firewall provides perimeter security # - UFW on VM: allow SSH (22) + gateway (18789); deny everything else inbound # - Docker CE for agent sandbox isolation # - Systemd hardening: NoNewPrivileges, PrivateTmp, ProtectSystem # # Signal channel MANUAL STEP required after deploy: # sudo -i -u openclaw # signal-cli link -n "OpenClaw" # scan QR with Signal app # openclaw pairing approve signal # # Play order: # Play 1: openclaw_create_vm — Create Ubuntu VM in Proxmox (cloud-init) # Play 2: openclaw_wait — Wait for SSH to become available # Play 3: openclaw_install — Install OpenClaw, security stack, Signal channel # # Usage: # ansible-navigator run playbooks/deploy_openclaw.yml # ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_create_vm # ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install # ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install,openclaw_signal # --------------------------------------------------------------------------- # Play 1: Create Ubuntu VM in Proxmox using cloud-init # --------------------------------------------------------------------------- - name: Create OpenClaw VM in Proxmox hosts: openclaw.toal.ca gather_facts: false connection: local tags: openclaw_create_vm vars: # Proxmox connection — override in host_vars if needed proxmox_node: pve1 proxmox_api_user: ansible@pam proxmox_api_token_id: ansible proxmox_api_token_secret: "{{ vault_proxmox_token_secret }}" proxmox_validate_certs: false proxmox_storage: local-lvm proxmox_iso_dir: /var/lib/vz/template/iso # VM spec — override in host_vars for the openclaw inventory host openclaw_vm_name: openclaw openclaw_vm_id: 0 openclaw_vm_cpu: 2 openclaw_vm_memory_mb: 4096 openclaw_vm_disk_gb: 40 openclaw_vm_vnet: lan openclaw_vm_user: ubuntu openclaw_vm_ssh_public_key: "" # required — set in host_vars openclaw_vm_ip: dhcp # set to x.x.x.x for static openclaw_vm_prefix: 24 openclaw_vm_gateway: "" openclaw_vm_nameserver: "" openclaw_vm_cloud_image_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" openclaw_vm_cloud_image_filename: noble-server-cloudimg-amd64.img # Computed __openclaw_proxmox_api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}" __openclaw_proxmox_api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}" tasks: - name: Download Ubuntu 24.04 cloud image to Proxmox host ansible.builtin.get_url: url: "{{ openclaw_vm_cloud_image_url }}" dest: "{{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }}" mode: "0644" delegate_to: proxmox_host - name: Create VM definition community.proxmox.proxmox_kvm: api_host: "{{ __openclaw_proxmox_api_host }}" api_user: "{{ proxmox_api_user }}" api_port: "{{ __openclaw_proxmox_api_port }}" api_token_id: "{{ proxmox_api_token_id }}" api_token_secret: "{{ proxmox_api_token_secret }}" validate_certs: "{{ proxmox_validate_certs }}" node: "{{ proxmox_node }}" vmid: "{{ openclaw_vm_id | default(omit, true) }}" name: "{{ openclaw_vm_name }}" cores: "{{ openclaw_vm_cpu }}" memory: "{{ openclaw_vm_memory_mb }}" cpu: host machine: q35 bios: ovmf efidisk0: storage: "{{ proxmox_storage }}" format: raw efitype: 4m pre_enrolled_keys: false scsihw: virtio-scsi-single net: net0: "virtio,bridge={{ openclaw_vm_vnet }}" boot: "order=scsi0" onboot: true state: present - name: Retrieve VM info community.proxmox.proxmox_vm_info: api_host: "{{ __openclaw_proxmox_api_host }}" api_user: "{{ proxmox_api_user }}" api_port: "{{ __openclaw_proxmox_api_port }}" api_token_id: "{{ proxmox_api_token_id }}" api_token_secret: "{{ proxmox_api_token_secret }}" validate_certs: "{{ proxmox_validate_certs }}" node: "{{ proxmox_node }}" name: "{{ openclaw_vm_name }}" type: qemu config: current register: __openclaw_vm_info retries: 5 - name: Set VM ID fact ansible.builtin.set_fact: openclaw_vm_id: "{{ __openclaw_vm_info.proxmox_vms[0].vmid }}" cacheable: true - name: Check if disk is already imported (scsi0 present in config) ansible.builtin.set_fact: __openclaw_disk_imported: "{{ __openclaw_vm_info.proxmox_vms[0].config.scsi0 is defined }}" - name: Import cloud image as primary disk ansible.builtin.command: cmd: >- qm importdisk {{ openclaw_vm_id }} {{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }} {{ proxmox_storage }} --format raw delegate_to: proxmox_host changed_when: true when: not __openclaw_disk_imported | bool - name: Attach imported disk as scsi0 ansible.builtin.command: cmd: "qm set {{ openclaw_vm_id }} --scsi0 {{ proxmox_storage }}:vm-{{ openclaw_vm_id }}-disk-0,iothread=1,cache=writeback" delegate_to: proxmox_host changed_when: true when: not __openclaw_disk_imported | bool - name: Resize disk to configured size ansible.builtin.command: cmd: "qm disk resize {{ openclaw_vm_id }} scsi0 {{ openclaw_vm_disk_gb }}G" delegate_to: proxmox_host changed_when: true when: not __openclaw_disk_imported | bool - name: Add cloud-init drive ansible.builtin.command: cmd: "qm set {{ openclaw_vm_id }} --ide2 {{ proxmox_storage }}:cloudinit" delegate_to: proxmox_host changed_when: true when: not __openclaw_disk_imported | bool - name: Write SSH public key to temp file on Proxmox host ansible.builtin.copy: content: "{{ openclaw_vm_ssh_public_key }}" dest: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub" mode: "0600" delegate_to: proxmox_host no_log: false - name: Configure cloud-init user and SSH key ansible.builtin.command: cmd: >- qm set {{ openclaw_vm_id }} --ciuser {{ openclaw_vm_user }} --sshkeys /tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub delegate_to: proxmox_host changed_when: true - name: Configure cloud-init network (static) ansible.builtin.command: cmd: >- qm set {{ openclaw_vm_id }} --ipconfig0 ip={{ openclaw_vm_ip }}/{{ openclaw_vm_prefix }},gw={{ openclaw_vm_gateway }} --nameserver {{ openclaw_vm_nameserver }} delegate_to: proxmox_host changed_when: true when: openclaw_vm_ip != 'dhcp' - name: Configure cloud-init network (DHCP) ansible.builtin.command: cmd: "qm set {{ openclaw_vm_id }} --ipconfig0 ip=dhcp" delegate_to: proxmox_host changed_when: true when: openclaw_vm_ip == 'dhcp' - name: Start VM community.proxmox.proxmox_kvm: api_host: "{{ __openclaw_proxmox_api_host }}" api_user: "{{ proxmox_api_user }}" api_port: "{{ __openclaw_proxmox_api_port }}" api_token_id: "{{ proxmox_api_token_id }}" api_token_secret: "{{ proxmox_api_token_secret }}" validate_certs: "{{ proxmox_validate_certs }}" node: "{{ proxmox_node }}" name: "{{ openclaw_vm_name }}" state: started - name: Remove temporary SSH key file ansible.builtin.file: path: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub" state: absent delegate_to: proxmox_host # --------------------------------------------------------------------------- # Play 2: Wait for VM to become reachable # --------------------------------------------------------------------------- - name: Wait for OpenClaw VM SSH hosts: openclaw.toal.ca gather_facts: false tags: openclaw_create_vm tasks: - name: Wait for SSH port ansible.builtin.wait_for_connection: timeout: 300 sleep: 10 # --------------------------------------------------------------------------- # Play 3: Install OpenClaw, security stack, and Signal channel # --------------------------------------------------------------------------- - name: Install and configure OpenClaw hosts: openclaw.toal.ca gather_facts: true become: true tags: openclaw_install roles: - role: openclaw