Initial commit — OpenClaw VM infrastructure

- ansible/: VM provisioning playbooks and roles
  - provision-vm.yml: create KVM VM from Ubuntu cloud image
  - install.yml: install OpenClaw on guest (upstream)
  - customize.yml: swappiness, virtiofs fstab, linger
  - roles/vm/: libvirt domain XML, cloud-init templates
  - inventory.yml + host_vars/zap.yml: zap instance config
- backup-openclaw-vm.sh: daily rsync + MinIO upload
- restore-openclaw-vm.sh: full redeploy from scratch
- README.md: full operational documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-03-12 12:18:31 -07:00
commit aceeb7b542
71 changed files with 7840 additions and 0 deletions
@@ -0,0 +1,69 @@
---
# Linux-specific Docker installation (apt-based)
- name: Install required system packages for Docker
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: true
- name: Create directory for Docker GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
creates: /etc/apt/keyrings/docker.gpg
executable: /bin/bash
- name: Add Docker repository
ansible.builtin.shell:
cmd: |
set -o pipefail
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/{{ ansible_distribution | lower }} \
$(lsb_release -cs) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
creates: /etc/apt/sources.list.d/docker.list
executable: /bin/bash
- name: Update apt cache after adding Docker repo
ansible.builtin.apt:
update_cache: true
- name: Install Docker CE
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
- name: Ensure Docker service is started and enabled
ansible.builtin.systemd:
name: docker
state: started
enabled: true
- name: Add user to docker group
ansible.builtin.user:
name: "{{ openclaw_user }}"
groups: docker
append: true
- name: Reset SSH connection to apply docker group
ansible.builtin.meta: reset_connection
@@ -0,0 +1,165 @@
---
# Linux-specific firewall configuration (UFW) with security hardening
# Install and configure fail2ban for SSH brute-force protection
- name: Install fail2ban
ansible.builtin.apt:
name: fail2ban
state: present
update_cache: true
- name: Configure fail2ban for SSH protection
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
content: |
# OpenClaw security hardening - SSH protection
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd
[sshd]
enabled = true
port = ssh
filter = sshd
# logpath not needed - systemd backend reads from journal
notify: Restart fail2ban
- name: Enable and start fail2ban
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: true
# Install and configure unattended-upgrades for automatic security updates
- name: Install unattended-upgrades
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present
- name: Configure automatic security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: '0644'
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
- name: Configure unattended-upgrades to only install security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: '0644'
content: |
// OpenClaw security hardening - automatic security updates
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
# UFW Firewall configuration
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: Set UFW default policies
community.general.ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: 'incoming', policy: 'deny' }
- { direction: 'outgoing', policy: 'allow' }
- { direction: 'routed', policy: 'deny' }
- name: Allow SSH on port 22
community.general.ufw:
rule: allow
port: '22'
proto: tcp
comment: 'SSH'
- name: Allow Tailscale UDP port 41641
community.general.ufw:
rule: allow
port: '41641'
proto: udp
comment: 'Tailscale'
when: tailscale_enabled | bool
- name: Get default network interface
ansible.builtin.shell:
cmd: |
set -o pipefail
ip route | grep default | awk '{print $5}' | head -n1
executable: /bin/bash
register: default_interface
changed_when: false
- name: Validate default network interface was detected
ansible.builtin.assert:
that:
- default_interface.stdout is defined
- default_interface.stdout | length > 0
fail_msg: "Failed to detect default network interface. Cannot configure firewall rules safely."
success_msg: "Default network interface detected: {{ default_interface.stdout }}"
- name: Create UFW after.rules for Docker isolation
ansible.builtin.blockinfile:
path: /etc/ufw/after.rules
marker: "# {mark} ANSIBLE MANAGED BLOCK - Docker isolation"
insertbefore: "^COMMIT$"
block: |
# Docker port isolation - block all forwarded traffic by default
:DOCKER-USER - [0:0]
# Allow established connections
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Allow localhost
-A DOCKER-USER -i lo -j ACCEPT
# Block all other forwarded traffic to Docker containers from external interface
-A DOCKER-USER -i {{ default_interface.stdout }} -j DROP
create: false
- name: Create Docker daemon config directory
ansible.builtin.file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon to work with UFW
ansible.builtin.template:
src: daemon.json.j2
dest: /etc/docker/daemon.json
owner: root
group: root
mode: '0644'
notify: Restart docker
- name: Enable UFW
community.general.ufw:
state: enabled
- name: Reload UFW
community.general.ufw:
state: reloaded
+24
View File
@@ -0,0 +1,24 @@
---
- name: Include system tools installation tasks
ansible.builtin.include_tasks: system-tools.yml
- name: Include Tailscale installation tasks
ansible.builtin.include_tasks: tailscale-linux.yml
when: tailscale_enabled | bool
- name: Include user creation tasks
ansible.builtin.include_tasks: user.yml
- name: Include Docker installation tasks
ansible.builtin.include_tasks: docker-linux.yml
when: not ci_test
- name: Include firewall configuration tasks
ansible.builtin.include_tasks: firewall-linux.yml
when: not ci_test
- name: Include Node.js installation tasks
ansible.builtin.include_tasks: nodejs.yml
- name: Include OpenClaw setup tasks
ansible.builtin.include_tasks: openclaw.yml
+69
View File
@@ -0,0 +1,69 @@
---
- name: Install required packages for Node.js
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
state: present
- name: Create directory for NodeSource GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add NodeSource GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
chmod a+r /etc/apt/keyrings/nodesource.gpg
creates: /etc/apt/keyrings/nodesource.gpg
executable: /bin/bash
- name: Add NodeSource repository
ansible.builtin.shell:
cmd: |
set -o pipefail
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] \
https://deb.nodesource.com/node_{{ nodejs_version }} nodistro main" | \
tee /etc/apt/sources.list.d/nodesource.list > /dev/null
creates: /etc/apt/sources.list.d/nodesource.list
executable: /bin/bash
- name: Update apt cache after adding NodeSource repo
ansible.builtin.apt:
update_cache: true
- name: Install Node.js
ansible.builtin.apt:
name: nodejs
state: present
- name: Check if pnpm is already installed
ansible.builtin.command: pnpm --version
register: pnpm_check
failed_when: false
changed_when: false
- name: Install pnpm globally
ansible.builtin.command: npm install -g pnpm
when: pnpm_check.rc != 0
- name: Verify Node.js installation
ansible.builtin.command: node --version
register: node_version
changed_when: false
- name: Verify pnpm installation
ansible.builtin.command: pnpm --version
register: pnpm_version
changed_when: false
- name: Display Node.js and pnpm versions
ansible.builtin.debug:
msg:
- "Node.js version: {{ node_version.stdout }}"
- "pnpm version: {{ pnpm_version.stdout }}"
@@ -0,0 +1,215 @@
---
# Development mode installation - Clone repo, build from source, link globally
- name: Create code directory
ansible.builtin.file:
path: "{{ openclaw_code_dir }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
- name: Check if openclaw repository already exists
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/.git"
register: openclaw_repo_exists
- name: Clone openclaw repository
ansible.builtin.git:
repo: "{{ openclaw_repo_url }}"
dest: "{{ openclaw_repo_dir }}"
version: "{{ openclaw_repo_branch }}"
update: true
become: true
become_user: "{{ openclaw_user }}"
when: not openclaw_repo_exists.stat.exists
- name: Pull latest changes if repo exists
ansible.builtin.git:
repo: "{{ openclaw_repo_url }}"
dest: "{{ openclaw_repo_dir }}"
version: "{{ openclaw_repo_branch }}"
update: true
become: true
become_user: "{{ openclaw_user }}"
when: openclaw_repo_exists.stat.exists
register: git_pull_result
- name: Display git pull status
ansible.builtin.debug:
msg: "Git repository updated: {{ git_pull_result.changed | default(false) }}"
when: openclaw_repo_exists.stat.exists
- name: Install dependencies with pnpm
ansible.builtin.shell:
cmd: pnpm install
chdir: "{{ openclaw_repo_dir }}"
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: pnpm_install_result
changed_when: "'Already up to date' not in pnpm_install_result.stdout"
- name: Build openclaw from source
ansible.builtin.shell:
cmd: pnpm build
chdir: "{{ openclaw_repo_dir }}"
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: pnpm_build_result
changed_when: true # Build always changes dist/ directory
- name: Display build output
ansible.builtin.debug:
msg: "Build completed successfully"
when: pnpm_build_result.rc == 0
- name: Check if dist directory exists
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/dist"
register: dist_dir
- name: Fail if build didn't create dist directory
ansible.builtin.fail:
msg: "Build failed - dist directory not found"
when: not dist_dir.stat.exists
- name: Check for package metadata
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/package.json"
register: openclaw_package_json_stat
- name: Read package metadata
ansible.builtin.slurp:
src: "{{ openclaw_repo_dir }}/package.json"
register: openclaw_package_json_raw
when: openclaw_package_json_stat.stat.exists
- name: Parse package metadata
ansible.builtin.set_fact:
openclaw_package_json: "{{ openclaw_package_json_raw.content | b64decode | from_json }}"
when: openclaw_package_json_stat.stat.exists
- name: Resolve metadata-defined CLI entrypoint
ansible.builtin.set_fact:
openclaw_package_bin_entry: >-
{{
openclaw_package_json.bin
if openclaw_package_json.bin is string else
(
openclaw_package_json.bin.openclaw
if openclaw_package_json.bin is mapping and 'openclaw' in openclaw_package_json.bin else
(
(openclaw_package_json.bin | dict2items | map(attribute='value') | list | first)
if openclaw_package_json.bin is mapping else
''
)
)
}}
when: openclaw_package_json_stat.stat.exists
- name: Build CLI entrypoint candidate list
ansible.builtin.set_fact:
openclaw_cli_candidate_paths: >-
{{
[
openclaw_package_bin_entry | default(''),
'openclaw.mjs',
'bin/openclaw.js',
'dist/index.js'
] | reject('equalto', '') | unique | list
}}
- name: Check possible OpenClaw CLI entrypoints
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/{{ item }}"
loop: "{{ openclaw_cli_candidate_paths }}"
register: openclaw_cli_candidates
- name: Resolve OpenClaw CLI entrypoint path
ansible.builtin.set_fact:
openclaw_cli_entry: >-
{{
(
openclaw_cli_candidates.results
| selectattr('stat.exists')
| map(attribute='stat.path')
| list
| first
) | default('')
}}
changed_when: false
- name: Fail if openclaw CLI entrypoint is missing
ansible.builtin.fail:
msg: >-
Unable to locate OpenClaw CLI entrypoint in {{ openclaw_repo_dir }}.
Expected one of: {{ openclaw_cli_candidate_paths | join(', ') }}
when: openclaw_cli_entry == ""
- name: Remove existing global openclaw symlink (if any)
ansible.builtin.file:
path: "{{ openclaw_home }}/.local/bin/openclaw"
state: absent
- name: Create symlink to openclaw binary
ansible.builtin.file:
src: "{{ openclaw_cli_entry }}"
dest: "{{ openclaw_home }}/.local/bin/openclaw"
state: link
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
force: true
- name: Make openclaw binary executable
ansible.builtin.file:
path: "{{ openclaw_cli_entry }}"
mode: '0755'
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
- name: Verify openclaw installation from development build
ansible.builtin.shell:
cmd: openclaw --version
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_dev_version
changed_when: false
- name: Display installed OpenClaw version (development build)
ansible.builtin.debug:
msg: |
OpenClaw installed from source: {{ openclaw_dev_version.stdout }}
Repository: {{ openclaw_repo_dir }}
Binary: {{ openclaw_home }}/.local/bin/openclaw -> {{ openclaw_cli_entry }}
- name: Add development mode info to .bashrc
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw development"
block: |
# OpenClaw development mode
export OPENCLAW_DEV_DIR="{{ openclaw_repo_dir }}"
# Aliases for development
alias openclaw-rebuild='cd {{ openclaw_repo_dir }} && pnpm build'
alias openclaw-dev='cd {{ openclaw_repo_dir }}'
alias openclaw-pull='cd {{ openclaw_repo_dir }} && git pull && pnpm install && pnpm build'
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
@@ -0,0 +1,32 @@
---
# Release mode installation - Install via pnpm from npm registry
- name: Install OpenClaw globally as openclaw user (using pnpm)
ansible.builtin.shell:
cmd: pnpm install -g openclaw@latest
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_install
changed_when: "'Already up to date' not in openclaw_install.stdout"
- name: Verify openclaw installation
ansible.builtin.shell:
cmd: openclaw --version
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_version
changed_when: false
- name: Display installed OpenClaw version (release)
ansible.builtin.debug:
msg: "OpenClaw installed from npm: {{ openclaw_version.stdout }}"
+121
View File
@@ -0,0 +1,121 @@
---
- name: Validate openclaw_install_mode
ansible.builtin.assert:
that:
- openclaw_install_mode in ["release", "development"]
fail_msg: "Invalid openclaw_install_mode: '{{ openclaw_install_mode }}'. Must be 'release' or 'development'."
success_msg: "Valid install mode: {{ openclaw_install_mode }}"
- name: Ensure openclaw home directory exists with correct ownership
ansible.builtin.file:
path: "{{ openclaw_home }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
- name: Create OpenClaw directories (structure only, no config files)
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: "{{ item.mode }}"
loop:
- { path: "{{ openclaw_config_dir }}", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/sessions", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/credentials", mode: '0700' }
- { path: "{{ openclaw_config_dir }}/data", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/logs", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents/main", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents/main/agent", mode: '0700' }
- { path: "{{ openclaw_config_dir }}/workspace", mode: '0755' }
- name: Create pnpm directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
loop:
- "{{ openclaw_home }}/.local/share/pnpm"
- "{{ openclaw_home }}/.local/share/pnpm/store"
- "{{ openclaw_home }}/.local/bin"
- name: Ensure pnpm directories have correct ownership
ansible.builtin.file:
path: "{{ openclaw_home }}/.local/share/pnpm"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
recurse: true
mode: '0755'
- name: Configure pnpm for openclaw user
ansible.builtin.shell:
cmd: |
CURRENT_GLOBAL_DIR=$(pnpm config get global-dir 2>/dev/null || echo "")
CURRENT_BIN_DIR=$(pnpm config get global-bin-dir 2>/dev/null || echo "")
CHANGED=0
if [ "$CURRENT_GLOBAL_DIR" != "{{ openclaw_home }}/.local/share/pnpm" ]; then
pnpm config set global-dir {{ openclaw_home }}/.local/share/pnpm
CHANGED=1
fi
if [ "$CURRENT_BIN_DIR" != "{{ openclaw_home }}/.local/bin" ]; then
pnpm config set global-bin-dir {{ openclaw_home }}/.local/bin
CHANGED=1
fi
exit $CHANGED
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
register: pnpm_config_result
changed_when: pnpm_config_result.rc == 1
failed_when: pnpm_config_result.rc > 1
- name: Display installation mode
ansible.builtin.debug:
msg: "Installation mode: {{ openclaw_install_mode }}"
# Include appropriate installation method based on mode
- name: Include release installation (pnpm install -g)
ansible.builtin.include_tasks: openclaw-release.yml
when: openclaw_install_mode == "release"
- name: Include development installation (git clone + build + link)
ansible.builtin.include_tasks: openclaw-development.yml
when: openclaw_install_mode == "development"
- name: Configure .bashrc for openclaw user (base config)
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw pnpm"
block: |
# pnpm configuration
export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm"
export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH"
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
insertafter: EOF
# NOTE: We do NOT create config.yml here - openclaw onboard/configure will do that
# We also do NOT install the systemd service - openclaw onboard --install-daemon will do that
# The .openclaw directory structure is created above, but config and daemon are user-initiated
- name: Display configuration note
ansible.builtin.debug:
msg: |
OpenClaw is installed but NOT configured yet.
Next steps (run as openclaw user):
1. Switch user: sudo su - {{ openclaw_user }}
2. Run onboarding: openclaw onboard --install-daemon
This will:
- Create configuration files (~/.openclaw/openclaw.json)
- Guide you through provider setup
- Install and start the daemon service automatically
@@ -0,0 +1,53 @@
---
# Linux-specific system tools installation (apt-based)
- name: Install essential system tools (Linux - apt)
ansible.builtin.apt:
name:
# Editors
- vim
- nano
# Version control
- git
- git-lfs
# Network tools
- curl
- wget
- netcat-openbsd
- net-tools
- dnsutils
- iputils-ping
- traceroute
- tcpdump
- nmap
- socat
- telnet
# Debugging tools
- strace
- lsof
- gdb
- htop
- iotop
- iftop
- sysstat
- procps
# System utilities
- tmux
- tree
- jq
- unzip
- rsync
- less
# Build essentials for development
- build-essential
- file
state: present
update_cache: true
- name: Deploy global vim configuration (Linux)
ansible.builtin.template:
src: vimrc.j2
dest: /etc/vim/vimrc.local
owner: root
group: root
mode: '0644'
@@ -0,0 +1,25 @@
---
# Main system tools orchestration - Linux only
- name: Include Linux system tools installation
ansible.builtin.include_tasks: system-tools-linux.yml
# Common tasks for all operating systems
- name: Configure git globally
community.general.git_config:
name: "{{ item.name }}"
scope: global
value: "{{ item.value }}"
loop:
- { name: 'init.defaultBranch', value: 'main' }
- { name: 'pull.rebase', value: 'false' }
- { name: 'core.editor', value: 'vim' }
- { name: 'color.ui', value: 'auto' }
- { name: 'alias.st', value: 'status' }
- { name: 'alias.co', value: 'checkout' }
- { name: 'alias.br', value: 'branch' }
- { name: 'alias.ci', value: 'commit' }
- { name: 'alias.unstage', value: 'reset HEAD --' }
- { name: 'alias.last', value: 'log -1 HEAD' }
- { name: 'alias.lg', value: 'log --oneline --graph --decorate --all' }
@@ -0,0 +1,61 @@
---
# Linux-specific Tailscale installation (Debian/Ubuntu)
- name: Add Tailscale GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
DIST="{{ ansible_distribution | lower }}"
RELEASE="{{ ansible_distribution_release }}"
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.noarmor.gpg" | \
tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
creates: /usr/share/keyrings/tailscale-archive-keyring.gpg
executable: /bin/bash
- name: Add Tailscale repository
ansible.builtin.shell:
cmd: |
set -o pipefail
DIST="{{ ansible_distribution | lower }}"
RELEASE="{{ ansible_distribution_release }}"
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.tailscale-keyring.list" | \
tee /etc/apt/sources.list.d/tailscale.list > /dev/null
creates: /etc/apt/sources.list.d/tailscale.list
executable: /bin/bash
- name: Update apt cache after adding Tailscale repo
ansible.builtin.apt:
update_cache: true
- name: Install Tailscale
ansible.builtin.apt:
name: tailscale
state: present
- name: Enable Tailscale service (Linux)
ansible.builtin.systemd:
name: tailscaled
enabled: true
state: started
- name: Check if Tailscale is already connected (Linux)
ansible.builtin.command: tailscale status --json
register: tailscale_status_linux
changed_when: false
failed_when: false
- name: Display Tailscale auth URL if not connected (Linux)
ansible.builtin.debug:
msg:
- "============================================"
- "Tailscale installed but not connected yet"
- "============================================"
- ""
- "To connect this machine to your Tailnet:"
- "Run: sudo tailscale up"
- ""
- "For unattended installation, use an auth key:"
- "sudo tailscale up --authkey tskey-auth-xxxxx"
- ""
- "Get auth key from: https://login.tailscale.com/admin/settings/keys"
when: tailscale_status_linux.rc != 0
+192
View File
@@ -0,0 +1,192 @@
---
- 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