Add readonly SSH audit user role and playbooks

This commit is contained in:
Haitao Pan 2026-04-10 11:08:47 +08:00
parent b8d93ec31c
commit 19e1f4ef1d
14 changed files with 649 additions and 22 deletions

View File

@ -4,7 +4,7 @@
The traffic billing stack now has a single aggregate playbook:
`deploy_traffic_billing_stack.yml`
`deploy_svc_plus_core_services_stack.yml`
It orchestrates these existing playbooks in dependency order:
@ -13,6 +13,7 @@ It orchestrates these existing playbooks in dependency order:
3. `deploy_accounts_svc_plus.yml`
4. `deploy_console_svc_plus.yml`
5. `deploy_agent_svc_plus.yml`
6. `deploy_xworkmate_bridge_vhosts.yml`
### Full stack deploy
@ -23,7 +24,7 @@ export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
export STACK_TARGET_HOST=jp_xhttp_contabo_host
export console_service_sync_dns=true
ansible-playbook -i inventory.ini deploy_traffic_billing_stack.yml
ansible-playbook -i inventory.ini deploy_svc_plus_core_services_stack.yml
```
`STACK_ENV_FILE=./.env` is optional. Use it when you want the aggregate playbook to read a local `.env` file; GitHub Actions or other CI runners can skip it and pass values with `-e` instead.
@ -39,7 +40,7 @@ export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
export console_service_sync_dns=true
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_traffic_billing_stack.yml
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
```
### Deploy only selected services
@ -51,14 +52,15 @@ Use `STACK_SERVICES` with a comma-separated list:
- `accounts`
- `console`
- `agent`
- `xworkmate-bridge`
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export STACK_TARGET_HOST=jp-xhttp-contabo.svc.plus
export STACK_SERVICES=xray-exporter,billing-service,agent
export STACK_SERVICES=xray-exporter,billing-service,agent,xworkmate-bridge
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_traffic_billing_stack.yml
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
```
### Notes
@ -68,6 +70,7 @@ ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_traffic_billin
- `console` now writes a Caddy fragment named like `<server-name>-<release_id>-<hostname>-<domain>.caddy` instead of managing the Caddy service container itself.
- `billing-service` requires `DATABASE_URL`.
- `xray-exporter` and `agent` require `INTERNAL_SERVICE_TOKEN`.
- `xworkmate-bridge` accepts `XWORKMATE_BRIDGE_HOSTS`, and also follows `STACK_TARGET_HOST` when you want to deploy the whole stack to one host.
### Deploy console to a specific host and sync DNS

24
create_audit_user.yml Normal file
View File

@ -0,0 +1,24 @@
---
- name: Create a root-managed SSH audit user on selected hosts
hosts: all
become: true
gather_facts: true
vars:
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
readonly_ssh_user_profile: audit
readonly_ssh_user_lock_password: true
readonly_ssh_user_manage_sudoers: true
readonly_ssh_user_authorized_keys: >-
{{
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
else []
}}
roles:
- role: readonly_ssh_user

View File

@ -0,0 +1,25 @@
---
- name: Create a readonly SSH user on selected hosts
hosts: all
become: true
gather_facts: true
vars:
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
readonly_ssh_user_profile: "{{ lookup('env', 'READONLY_SSH_USER_PROFILE') | default('readonly', true) }}"
readonly_ssh_user_password_hash: "{{ lookup('env', 'READONLY_SSH_USER_PASSWORD_HASH') | default('', true) }}"
readonly_ssh_user_lock_password: "{{ lookup('env', 'READONLY_SSH_LOCK_PASSWORD') | default('true', true) | bool }}"
readonly_ssh_user_manage_sudoers: "{{ lookup('env', 'READONLY_SSH_ENABLE_SUDO') | default('false', true) | bool }}"
readonly_ssh_user_authorized_keys: >-
{{
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
else []
}}
roles:
- role: readonly_ssh_user

View File

@ -13,7 +13,7 @@
| default('', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent', true) }}
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'xray-exporter' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_billing_service.yml
@ -35,7 +35,7 @@
| default('http://127.0.0.1:8080', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent', true) }}
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'billing-service' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_accounts_svc_plus.yml
@ -57,7 +57,7 @@
| default('latest', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent', true) }}
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'accounts' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_console_svc_plus.yml
@ -87,7 +87,7 @@
| default('', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent', true) }}
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'console' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_agent_svc_plus.yml
@ -109,5 +109,16 @@
| default('http://127.0.0.1:8081', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent', true) }}
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'agent' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_xworkmate_bridge_vhosts.yml
vars:
xworkmate_bridge_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_HOSTS')
| default('all', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
when: "'xworkmate-bridge' in (stack_services.split(',') | map('trim') | list)"

View File

@ -0,0 +1,128 @@
# readonly_ssh_user
Create a remote SSH login user that can inspect the host as an unprivileged account but cannot modify protected system configuration.
## TLDR
export READONLY_SSH_USER_NAME=readonly Or auditor
export READONLY_SSH_LOCK_PASSWORD=true
export READONLY_SSH_ENABLE_SUDO=true
export READONLY_SSH_USER_PUBLIC_KEY='你的ssh公钥'
ansible-playbook -i inventory.ini create_readonly_ssh_user.yml --limit jp-xhttp-contabo.svc.plus
默认是 `readonly` profile。要创建更实用的“审计用户”加上
```bash
export READONLY_SSH_USER_PROFILE=audit
```
## What it does
- Creates a normal Linux user with no `sudo` or other privileged groups by default
- Locks the account password by default so password login is not usable
- Supports login with either a password hash or one or more SSH public keys
- Writes an `sshd_config.d` drop-in with per-user SSH restrictions
- Removes the user from common privileged groups such as `sudo`, `wheel`, and `docker`
- Optionally grants `sudo` for a tightly scoped read-only command whitelist
- Supports two profiles:
- `readonly`: minimal read-only sudo whitelist
- `audit`: broader inspection whitelist for logs, units, network, users, and common service status checks
## Important limitation
Linux 没法只靠“普通用户 + sudo 白名单”创建一个绝对不可变更的一般用途 SSH 审计用户。这个 role 提供的是“受限审计用户”,不是内核级只读沙箱。
This role creates an unprivileged user, not a kernel-enforced immutable sandbox. The user can still write to:
- its own home directory
- any path that is already world-writable or group-writable to one of its groups
It will not be able to change protected system configuration unless you explicitly grant extra privileges.
If you enable limited sudo, the user still does not join the `sudo` group. Instead, the role writes a dedicated sudoers file with only approved read-only commands.
For the default hardening mode used for `readonly`, the intended model is:
- `passwd -l` style locked password
- SSH public key login only
- no password-based SSH login
- account lifecycle managed by `root`
`audit` profile 适合:
- 看系统配置
- 看服务状态
- 看日志
- 看网络和用户状态
- 做人工巡检
`audit` profile 不适合:
- 真正执行 `ansible-playbook` 去修配置
- 依赖 `become` 修改远端文件
- 重启服务、安装包、改权限、写 `/etc`
如果目标是“能用 Ansible 修复”,那已经不是审计用户,而是受限运维用户,需要单独设计 sudoers 白名单。
## Variables
- `readonly_ssh_user_profile`: `readonly` or `audit`, default `readonly`
- `readonly_ssh_user_name`: username, default `readonly`
- `readonly_ssh_user_password_hash`: password hash for SSH password login
- `readonly_ssh_user_lock_password`: lock the local password, default `true`
- `readonly_ssh_user_authorized_keys`: list of SSH public keys for key-based login
- `readonly_ssh_user_groups`: supplementary groups to keep, default `[]`
- `readonly_ssh_user_manage_sshd`: whether to write an SSH Match block, default `true`
- `readonly_ssh_user_manage_sudoers`: whether to create a limited sudoers rule, default `false`
- `readonly_ssh_user_sudo_commands_readonly`: minimal whitelist
- `readonly_ssh_user_sudo_commands_audit`: broader audit whitelist
- `readonly_ssh_user_sudo_commands`: final whitelist, auto-derived from profile unless you override it
- `readonly_ssh_user_allow_tcp_forwarding`: default `false`
- `readonly_ssh_user_x11_forwarding`: default `false`
- `readonly_ssh_user_allow_agent_forwarding`: default `false`
- `readonly_ssh_user_force_command`: optional forced command
## Example playbook
```yaml
- hosts: jp_xhttp_contabo_host
become: true
roles:
- role: readonly_ssh_user
vars:
readonly_ssh_user_profile: audit
readonly_ssh_user_name: readonly
readonly_ssh_user_manage_sudoers: true
readonly_ssh_user_authorized_keys:
- "ssh-ed25519 AAAA..."
```
## About ansible-playbook -D -C
这个角色创建的 `audit` 用户可以辅助“看”:
- 读取配置
- 看日志和 unit 状态
- 运行手工审计命令
但它不应该被视为可以可靠执行 `ansible-playbook -D -C` 来“修正 role / playbook”的用户。原因是很多 Ansible 任务即使在 check mode 下,仍然需要更广的 `become` 能力、远端临时文件操作和模块执行权限。
如果你需要“Ansible 可修复但受限”的账号,建议单独建一个 `operator_lite` 角色,而不是继续扩大审计账号权限。
## Password hash example
Generate a password hash locally:
```bash
python3 -c 'import crypt, getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
```
## Root-managed SSH-only mode
The default role behavior now matches this model:
- the `readonly` user has a locked password
- SSH login is expected to happen by public key only
- `PasswordAuthentication no` is enforced for that user in the SSH Match block
- password resets and account management should be performed by `root`

View File

@ -0,0 +1,111 @@
---
readonly_ssh_user_profile: readonly
readonly_ssh_user_name: readonly
readonly_ssh_user_comment: "Read-only SSH user"
readonly_ssh_user_shell: /bin/bash
readonly_ssh_user_home: "/home/{{ readonly_ssh_user_name }}"
readonly_ssh_user_create_home: true
readonly_ssh_user_password_hash: ""
readonly_ssh_user_lock_password: true
readonly_ssh_user_authorized_keys: []
readonly_ssh_user_append_groups: false
readonly_ssh_user_groups: []
readonly_ssh_user_restricted_groups:
- sudo
- wheel
- adm
- docker
- lxd
- libvirt
- root
readonly_ssh_user_state: present
readonly_ssh_user_manage_sshd: true
readonly_ssh_user_sshd_dropin_dir: /etc/ssh/sshd_config.d
readonly_ssh_user_sshd_dropin_file: "99-{{ readonly_ssh_user_name }}-readonly.conf"
readonly_ssh_user_allow_tcp_forwarding: false
readonly_ssh_user_x11_forwarding: false
readonly_ssh_user_permit_tunnel: false
readonly_ssh_user_permit_tty: true
readonly_ssh_user_allow_agent_forwarding: false
readonly_ssh_user_password_authentication: false
readonly_ssh_user_pubkey_authentication: true
readonly_ssh_user_force_command: ""
readonly_ssh_service_name_override: ""
readonly_ssh_user_manage_sudoers: false
readonly_ssh_user_sudo_nopasswd: true
readonly_ssh_user_sudoers_file: "/etc/sudoers.d/{{ readonly_ssh_user_name }}-readonly"
readonly_ssh_user_sudo_commands_readonly:
- /usr/bin/cat *
- /usr/bin/head *
- /usr/bin/tail *
- /usr/bin/grep *
- /usr/bin/find *
- /usr/bin/ls *
- /usr/bin/stat *
- /usr/bin/du *
- /usr/bin/df *
- /usr/bin/ps *
- /usr/bin/ss *
- /usr/bin/free
- /usr/bin/uptime
- /usr/bin/id
- /usr/bin/uname -a
- /usr/bin/hostnamectl status
- /usr/bin/systemctl status *
- /usr/bin/systemctl show *
- /usr/bin/journalctl *
readonly_ssh_user_sudo_commands_audit:
- /usr/bin/cat *
- /usr/bin/head *
- /usr/bin/tail *
- /usr/bin/grep *
- /usr/bin/egrep *
- /usr/bin/fgrep *
- /usr/bin/find *
- /usr/bin/ls *
- /usr/bin/stat *
- /usr/bin/namei *
- /usr/bin/file *
- /usr/bin/du *
- /usr/bin/df *
- /usr/bin/ps *
- /usr/bin/ss *
- /usr/bin/free
- /usr/bin/uptime
- /usr/bin/id
- /usr/bin/uname -a
- /usr/bin/hostnamectl status
- /usr/bin/systemctl status *
- /usr/bin/systemctl show *
- /usr/bin/systemctl list-units *
- /usr/bin/systemctl list-unit-files *
- /usr/bin/systemctl cat *
- /usr/bin/journalctl *
- /usr/bin/loginctl *
- /usr/bin/env
- /usr/bin/printenv
- /usr/bin/whoami
- /usr/bin/w
- /usr/bin/who
- /usr/bin/last *
- /usr/bin/lastlog *
- /usr/bin/passwd -S *
- /usr/bin/getent *
- /usr/bin/crontab -l *
- /usr/sbin/ufw status
- /usr/sbin/ip addr *
- /usr/sbin/ip route *
- /usr/sbin/ip rule *
- /usr/sbin/iptables -S *
- /usr/sbin/ip6tables -S *
- /usr/sbin/nginx -T
- /usr/sbin/apachectl -S
- /usr/bin/docker ps *
- /usr/bin/docker images *
- /usr/bin/docker inspect *
readonly_ssh_user_sudo_commands: >-
{{
readonly_ssh_user_sudo_commands_audit
if readonly_ssh_user_profile == 'audit'
else readonly_ssh_user_sudo_commands_readonly
}}

View File

@ -0,0 +1,33 @@
---
- name: Validate sshd configuration syntax
ansible.builtin.command: sshd -t
changed_when: false
when: not ansible_check_mode
listen: reload sshd
- name: Collect service facts for ssh reload
ansible.builtin.service_facts:
changed_when: false
listen: reload sshd
- name: Select SSH service name for readonly user role
ansible.builtin.set_fact:
readonly_ssh_service_name: >-
{{
readonly_ssh_service_name_override
if readonly_ssh_service_name_override | length > 0
else ('ssh' if 'ssh.service' in ansible_facts.services else 'sshd')
}}
listen: reload sshd
- name: Reload SSH service
ansible.builtin.service:
name: "{{ readonly_ssh_service_name }}"
state: reloaded
listen: reload sshd
- name: Validate sudoers syntax
ansible.builtin.command: "visudo -cf {{ readonly_ssh_user_sudoers_file }}"
changed_when: false
when: not ansible_check_mode
listen: validate sudoers

View File

@ -0,0 +1,167 @@
# readonly audit checklist
Use this checklist after creating a `readonly` or `audit` SSH user with the `readonly_ssh_user` role.
The goal is to confirm the account can inspect system information and logs, but cannot make real changes such as editing protected files, restarting services, installing packages, or changing permissions.
## 1. Login
```bash
ssh readonly@jp-xhttp-contabo.svc.plus
```
## 2. Verify identity and sudo scope
These commands should succeed:
```bash
whoami
id
sudo -l
```
Expected:
- current user is `readonly`
- user is not in `sudo`, `wheel`, `docker`, or other privileged groups unless explicitly intended
- `sudo -l` shows only the approved read-only whitelist
## 3. Verify system and environment inspection
These commands should succeed:
```bash
uname -a
hostnamectl status
uptime
free -h
df -h
sudo env
```
Expected:
- system identity and runtime information are visible
- no write action is performed
## 4. Verify user, session, and scheduled task inspection
These commands should succeed:
```bash
sudo getent passwd | head
sudo last -n 20
sudo lastlog | head
sudo crontab -l -u root
```
Expected:
- account and login history can be reviewed
- root cron can be inspected if included in the sudo whitelist
## 5. Verify network inspection
These commands should succeed:
```bash
sudo ip addr
sudo ip route
sudo ss -tulpn
sudo iptables -S
```
Expected:
- interface, route, listening port, and firewall rule information is visible
## 6. Verify service and log inspection
These commands should succeed:
```bash
sudo systemctl status ssh
sudo systemctl list-units --type=service --no-pager | head -50
sudo journalctl -u ssh -n 50 --no-pager
```
Expected:
- service status is visible
- logs can be inspected
- no service control actions are available
## 7. Verify configuration file inspection
These commands should succeed:
```bash
sudo cat /etc/ssh/sshd_config
sudo cat /etc/passwd
sudo nginx -T
```
Expected:
- protected configuration can be inspected through approved read-only commands
- config dump commands such as `nginx -T` work if the binary exists and is whitelisted
## 8. Verify container inspection if Docker is present
These commands should succeed when Docker is installed and included in the whitelist:
```bash
sudo docker ps
sudo docker images
sudo docker inspect <container_or_image>
```
Expected:
- container and image metadata is visible
- no container lifecycle actions are permitted
## 9. Verify modification attempts are blocked
These commands should fail:
```bash
sudo systemctl restart ssh
sudo systemctl reload nginx
sudo touch /etc/readonly-test
sudo cp /etc/hosts /etc/hosts.bak2
sudo chmod 644 /etc/ssh/sshd_config
sudo useradd test123
sudo apt install -y tree
```
Expected:
- restart and reload commands fail
- writes under `/etc` fail
- permission changes fail
- package installation fails
- user creation fails
## 10. Interpretation
The account is behaving correctly if:
- inspection commands succeed
- modification commands fail
- the account can read most operational information needed for audits
- the account still cannot apply real remediation
If `ansible-playbook -D -C` is the target use case, this account is still not the right choice for remediation. It is an audit account, not a limited operator account.
## 11. Optional follow-up
If the account is too weak for your audit process:
- add only specific additional read-only commands to the sudo whitelist
- avoid broad additions such as editor binaries, package managers, service restart commands, shell escapes, or file write utilities
If the account is too strong:
- remove commands from the `audit` whitelist
- create a stricter profile derived from `readonly`

View File

@ -0,0 +1,114 @@
---
- name: Assert readonly SSH user profile is supported
ansible.builtin.assert:
that:
- readonly_ssh_user_profile in ['readonly', 'audit']
fail_msg: "readonly_ssh_user_profile must be one of: readonly, audit"
- name: Assert readonly SSH user has at least one login method
ansible.builtin.assert:
that:
- readonly_ssh_user_authorized_keys | length > 0 or (readonly_ssh_user_password_hash | length > 0 and not readonly_ssh_user_lock_password)
fail_msg: >-
Set readonly_ssh_user_authorized_keys for SSH key login, or provide an
unlocked readonly_ssh_user_password_hash if you intentionally want password login.
- name: Set role comment based on selected profile when default is unchanged
ansible.builtin.set_fact:
readonly_ssh_user_effective_comment: >-
{{
'Audit SSH user'
if readonly_ssh_user_profile == 'audit'
else 'Read-only SSH user'
}}
- name: Compute effective sudo command whitelist
ansible.builtin.set_fact:
readonly_ssh_user_effective_sudo_commands: "{{ readonly_ssh_user_sudo_commands }}"
- name: Ensure readonly SSH user exists
ansible.builtin.user:
name: "{{ readonly_ssh_user_name }}"
comment: >-
{{
readonly_ssh_user_comment
if readonly_ssh_user_comment != 'Read-only SSH user'
else readonly_ssh_user_effective_comment
}}
shell: "{{ readonly_ssh_user_shell }}"
home: "{{ readonly_ssh_user_home }}"
create_home: "{{ readonly_ssh_user_create_home }}"
password: "{{ readonly_ssh_user_password_hash if readonly_ssh_user_password_hash | length > 0 else omit }}"
password_lock: "{{ readonly_ssh_user_lock_password }}"
groups: "{{ readonly_ssh_user_groups }}"
append: "{{ readonly_ssh_user_append_groups }}"
state: "{{ readonly_ssh_user_state }}"
- name: Ensure SSH directory exists for readonly user
ansible.builtin.file:
path: "{{ readonly_ssh_user_home }}/.ssh"
state: directory
owner: "{{ readonly_ssh_user_name }}"
group: "{{ readonly_ssh_user_name }}"
mode: "0700"
when: readonly_ssh_user_authorized_keys | length > 0
- name: Install authorized keys for readonly user
ansible.posix.authorized_key:
user: "{{ readonly_ssh_user_name }}"
key: "{{ item }}"
state: present
manage_dir: false
loop: "{{ readonly_ssh_user_authorized_keys }}"
when: readonly_ssh_user_authorized_keys | length > 0
- name: Remove readonly user from privileged groups
ansible.builtin.command: "gpasswd -d {{ readonly_ssh_user_name }} {{ item }}"
register: readonly_user_group_removal
failed_when: readonly_user_group_removal.rc not in [0, 3]
changed_when: readonly_user_group_removal.rc == 0
loop: "{{ readonly_ssh_user_restricted_groups }}"
- name: Ensure sshd drop-in directory exists
ansible.builtin.file:
path: "{{ readonly_ssh_user_sshd_dropin_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: readonly_ssh_user_manage_sshd
- name: Configure readonly user SSH restrictions
ansible.builtin.copy:
dest: "{{ readonly_ssh_user_sshd_dropin_dir }}/{{ readonly_ssh_user_sshd_dropin_file }}"
owner: root
group: root
mode: "0644"
content: |
Match User {{ readonly_ssh_user_name }}
PasswordAuthentication {{ 'yes' if readonly_ssh_user_password_authentication else 'no' }}
PubkeyAuthentication {{ 'yes' if readonly_ssh_user_pubkey_authentication else 'no' }}
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitEmptyPasswords no
AllowTcpForwarding {{ 'yes' if readonly_ssh_user_allow_tcp_forwarding else 'no' }}
X11Forwarding {{ 'yes' if readonly_ssh_user_x11_forwarding else 'no' }}
PermitTunnel {{ 'yes' if readonly_ssh_user_permit_tunnel else 'no' }}
PermitTTY {{ 'yes' if readonly_ssh_user_permit_tty else 'no' }}
AllowAgentForwarding {{ 'yes' if readonly_ssh_user_allow_agent_forwarding else 'no' }}
{% if readonly_ssh_user_force_command | length > 0 %}
ForceCommand {{ readonly_ssh_user_force_command }}
{% endif %}
notify: reload sshd
when: readonly_ssh_user_manage_sshd
- name: Configure readonly sudo command whitelist
ansible.builtin.copy:
dest: "{{ readonly_ssh_user_sudoers_file }}"
owner: root
group: root
mode: "0440"
content: |
{{ readonly_ssh_user_name }} ALL=(root) {{ 'NOPASSWD:' if readonly_ssh_user_sudo_nopasswd else '' }} {{ readonly_ssh_user_effective_sudo_commands | join(', ') }}
notify: validate sudoers
when: readonly_ssh_user_manage_sudoers

View File

@ -1,8 +1,11 @@
---
acp_codex_service_name: codex-app-server
acp_codex_service_user: root
acp_codex_service_group: root
acp_codex_workdir: /root
acp_codex_runtime_user: ubuntu
acp_codex_runtime_group: "{{ acp_codex_runtime_user }}"
acp_codex_runtime_home: "/home/{{ acp_codex_runtime_user }}"
acp_codex_service_user: "{{ acp_codex_runtime_user }}"
acp_codex_service_group: "{{ acp_codex_runtime_group }}"
acp_codex_workdir: "{{ acp_codex_runtime_home }}"
acp_codex_listen_host: 127.0.0.1
acp_codex_listen_port: 9001
acp_codex_bridge_service_name: acp-bridge-codex
@ -22,7 +25,7 @@ acp_codex_bridge_allowed_origins:
- http://localhost:*
- http://127.0.0.1:*
acp_codex_environment:
CODEX_HOME: "{{ acp_codex_workdir }}/.codex"
CODEX_HOME: "{{ acp_codex_runtime_home }}/.codex"
OPENAI_API_KEY: "{{ lookup('ansible.builtin.env', 'OPENAI_API_KEY') | default('', true) }}"
OPENAI_BASE_URL: "{{ lookup('ansible.builtin.env', 'OPENAI_BASE_URL') | default('', true) }}"
OPENAI_BASE_API_URL: "{{ lookup('ansible.builtin.env', 'OPENAI_BASE_API_URL') | default('', true) }}"

View File

@ -5,8 +5,8 @@ Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
User={{ acp_codex_service_user }}
Group={{ acp_codex_service_group }}
WorkingDirectory={{ acp_codex_workdir }}
Environment=HOME={{ acp_codex_workdir }}
Environment=TERM=xterm-256color

View File

@ -1,9 +1,12 @@
---
acp_gemini_service_name: acp-gemini-adapter
acp_gemini_service_user: root
acp_gemini_service_group: root
acp_gemini_home: /root
acp_gemini_workdir: /root
acp_gemini_service_user: ubuntu
acp_gemini_service_group: "{{ acp_gemini_service_user }}"
acp_gemini_home: "/home/{{ acp_gemini_service_user }}"
acp_gemini_workdir: "{{ acp_gemini_home }}"
acp_gemini_xdg_config_home: "{{ acp_gemini_home }}/.config"
acp_gemini_xdg_state_home: "{{ acp_gemini_home }}/.local/state"
acp_gemini_config_dir: "{{ acp_gemini_home }}/.gemini"
acp_gemini_binary_path: /usr/bin/gemini
acp_gemini_args: --experimental-acp
@ -23,6 +26,9 @@ acp_gemini_public_base_url: https://acp-server.svc.plus/gemini
acp_gemini_manage_caddy: false
acp_gemini_auth_token: "{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true) }}"
acp_gemini_environment:
XDG_CONFIG_HOME: "{{ acp_gemini_xdg_config_home }}"
XDG_STATE_HOME: "{{ acp_gemini_xdg_state_home }}"
GEMINI_CONFIG_DIR: "{{ acp_gemini_config_dir }}"
GEMINI_ADAPTER_AUTH_TOKEN: "{{ acp_gemini_auth_token }}"
ACP_GEMINI_BIN: "{{ acp_gemini_binary_path }}"
acp_gemini_packages:

View File

@ -24,8 +24,8 @@
ansible.builtin.copy:
src: "{{ acp_gemini_bridge_local_binary_path }}"
dest: "{{ acp_gemini_bridge_binary_path }}"
owner: root
group: root
owner: "{{ acp_gemini_service_user }}"
group: "{{ acp_gemini_service_group }}"
mode: "0755"
notify: Restart gemini acp adapter

View File

@ -57,6 +57,8 @@ Behavior after deployment:
- requests with `Authorization: Bearer $INTERNAL_SERVICE_TOKEN` are accepted
- this playbook only defines and validates the shared ingress token path
- provider-specific authentication and ACP method compatibility are intentionally left to the individual runtimes
- the Codex runtime user is a role variable and defaults to `ubuntu`, so it can be changed from inventory if needed
- Gemini adapter is now also aligned to `ubuntu` home paths so it can reuse `/home/ubuntu/.gemini/oauth_creds.json`
## Public Endpoints