unoverse 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/unoverse.mjs +1 -1
- package/lib/create.mjs +1 -1
- package/operator/ansible/README.md +66 -0
- package/operator/ansible/ansible.cfg +19 -0
- package/operator/ansible/playbooks/backup.yml +47 -0
- package/operator/ansible/playbooks/db-setup.yml +123 -0
- package/operator/ansible/playbooks/deploy-images.yml +71 -0
- package/operator/ansible/playbooks/harden.yml +223 -0
- package/operator/ansible/playbooks/install.yml +236 -0
- package/operator/ansible/playbooks/relocate-db.yml +176 -0
- package/operator/ansible/playbooks/restore.yml +108 -0
- package/operator/ansible/playbooks/rollback.yml +55 -0
- package/operator/ansible/playbooks/test-connectivity.yml +269 -0
- package/operator/lib/deploy.sh +4 -1
- package/operator/lib/init.sh +1 -1
- package/operator/lib/logs.sh +3 -1
- package/operator/operator.sh +2 -17
- package/package.json +1 -1
- package/operator/lib/dashboard.sh +0 -85
- package/operator/lib/update.sh +0 -357
package/bin/unoverse.mjs
CHANGED
|
@@ -60,7 +60,7 @@ const UNIVERSE = findUniverse();
|
|
|
60
60
|
const OPERATOR_COMMANDS = new Set([
|
|
61
61
|
"start", "stop", "check", "logs", "deploy",
|
|
62
62
|
// kept working, not advertised
|
|
63
|
-
"ground", "dev", "build", "publish", "init",
|
|
63
|
+
"ground", "dev", "build", "publish", "init",
|
|
64
64
|
]);
|
|
65
65
|
|
|
66
66
|
const [, , cmd, ...args] = process.argv;
|
package/lib/create.mjs
CHANGED
|
@@ -256,7 +256,7 @@ export async function create(nameArg) {
|
|
|
256
256
|
env: { ...process.env, UNOVERSE_DOCR_TOKEN: token },
|
|
257
257
|
});
|
|
258
258
|
if (r.status !== 0) {
|
|
259
|
-
fail(`setup did not finish. Run 'unoverse
|
|
259
|
+
fail(`setup did not finish. Run 'unoverse start'${name === "." ? "" : ` in ./${name}`} to pick it back up`);
|
|
260
260
|
process.exit(r.status ?? 1);
|
|
261
261
|
}
|
|
262
262
|
console.log(`
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Gravity Platform Ansible Automation
|
|
2
|
+
|
|
3
|
+
Ansible playbooks for deployment, upgrades, and operations.
|
|
4
|
+
|
|
5
|
+
## Quick Start (Single VM)
|
|
6
|
+
|
|
7
|
+
There is no inventory to configure — deployments are single-VM by contract
|
|
8
|
+
(INFRASTRUCTURE.md) and `unoverse deploy` builds a temporary inventory from
|
|
9
|
+
`.env.production` each run.
|
|
10
|
+
Just use `.env.production` at the project root:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Configure production environment (Terraform renders it, complete)
|
|
14
|
+
cd infra/digitalocean && terraform apply
|
|
15
|
+
terraform output -raw env_production > ../../.env.production
|
|
16
|
+
|
|
17
|
+
# 2. Deploy
|
|
18
|
+
gravity deploy
|
|
19
|
+
|
|
20
|
+
# 3. Verify
|
|
21
|
+
gravity deploy test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The `gravity deploy` command reads `.env.production`, builds a temporary inventory, and runs the playbooks automatically.
|
|
25
|
+
|
|
26
|
+
## Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
ansible/
|
|
30
|
+
├── playbooks/ # All playbooks
|
|
31
|
+
├── templates/
|
|
32
|
+
└── ansible.cfg
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Direct Ansible Usage
|
|
36
|
+
|
|
37
|
+
For advanced use or multi-VM enterprise deployments:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd ansible
|
|
41
|
+
|
|
42
|
+
# Single VM (reads .env.production from project root)
|
|
43
|
+
|
|
44
|
+
# Multi-VM (target specific groups)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Playbooks
|
|
48
|
+
|
|
49
|
+
| Playbook | Purpose |
|
|
50
|
+
| ----------------------- | -------------------------------------------- |
|
|
51
|
+
| `install.yml` | Fresh install (Docker, images, services) |
|
|
52
|
+
| `deploy-packages.yml` | Deploy packages (rsync from local + build) |
|
|
53
|
+
| `db-setup.yml` | Database setup and migrations |
|
|
54
|
+
| `relocate-db.yml` | RELOCATE a database (dump/restore to another server) — NOT schema migrations (that is db-setup) |
|
|
55
|
+
| `rollback.yml` | Rollback to previous version |
|
|
56
|
+
| `health-check.yml` | Verify all services healthy |
|
|
57
|
+
| `backup.yml` | Backup UMAP models |
|
|
58
|
+
| `restore.yml` | Restore UMAP models from backup |
|
|
59
|
+
| `harden.yml` | Security hardening (SSH, firewall, fail2ban) |
|
|
60
|
+
| `test-connectivity.yml` | Test VM connectivity and ports |
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Ansible 2.12+
|
|
65
|
+
- SSH access to target VMs
|
|
66
|
+
- DOCR token (for pulling Docker images from DigitalOcean Container Registry)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[defaults]
|
|
2
|
+
# inventory: none by default — `unoverse deploy` generates a temp inventory from
|
|
3
|
+
# .env.production (multi-VM inventory retired to _legacy/ansible 2026-07-28)
|
|
4
|
+
roles_path = roles
|
|
5
|
+
host_key_checking = False
|
|
6
|
+
retry_files_enabled = False
|
|
7
|
+
# Silence the noisy ansible-core module deprecation warnings (to_text/to_bytes imports in
|
|
8
|
+
# the synchronize module) — they're upstream, not our playbooks. Keeps the deploy log clean.
|
|
9
|
+
deprecation_warnings = False
|
|
10
|
+
gathering = smart
|
|
11
|
+
fact_caching = jsonfile
|
|
12
|
+
fact_caching_connection = /tmp/ansible_facts
|
|
13
|
+
fact_caching_timeout = 3600
|
|
14
|
+
|
|
15
|
+
[privilege_escalation]
|
|
16
|
+
become = True
|
|
17
|
+
become_method = sudo
|
|
18
|
+
become_user = root
|
|
19
|
+
become_ask_pass = False
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Gravity Platform Backup Playbook
|
|
3
|
+
# Backs up UMAP models (database backup is customer responsibility)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# POC: ansible-playbook -i inventory/production.yml playbooks/backup.yml
|
|
7
|
+
# Enterprise: ansible-playbook -i inventory/production.yml playbooks/backup.yml -l ml_vms
|
|
8
|
+
|
|
9
|
+
- name: Backup Gravity Platform
|
|
10
|
+
hosts: all
|
|
11
|
+
become: yes
|
|
12
|
+
vars:
|
|
13
|
+
backup_dir: /opt/gravity/backups
|
|
14
|
+
backup_timestamp: "{{ ansible_date_time.iso8601_basic_short }}"
|
|
15
|
+
|
|
16
|
+
tasks:
|
|
17
|
+
- name: Create backup directory
|
|
18
|
+
file:
|
|
19
|
+
path: "{{ backup_dir }}"
|
|
20
|
+
state: directory
|
|
21
|
+
mode: "0755"
|
|
22
|
+
|
|
23
|
+
- name: Backup UMAP models
|
|
24
|
+
archive:
|
|
25
|
+
path: /opt/gravity/umap/models
|
|
26
|
+
dest: "{{ backup_dir }}/umap-models-{{ backup_timestamp }}.tar.gz"
|
|
27
|
+
format: gz
|
|
28
|
+
when: "'umap' in gravity_services"
|
|
29
|
+
|
|
30
|
+
- name: List backups
|
|
31
|
+
find:
|
|
32
|
+
paths: "{{ backup_dir }}"
|
|
33
|
+
patterns: "*.tar.gz,*.dump"
|
|
34
|
+
register: backup_files
|
|
35
|
+
|
|
36
|
+
- name: Display backup info
|
|
37
|
+
debug:
|
|
38
|
+
msg: |
|
|
39
|
+
Backup completed:
|
|
40
|
+
- UMAP Models: umap-models-{{ backup_timestamp }}.tar.gz
|
|
41
|
+
- Location: {{ backup_dir }}
|
|
42
|
+
- Total backups: {{ backup_files.files | length }}
|
|
43
|
+
|
|
44
|
+
- name: Clean old backups (keep last 7)
|
|
45
|
+
shell: |
|
|
46
|
+
ls -t {{ backup_dir }}/umap-models-*.tar.gz | tail -n +8 | xargs -r rm
|
|
47
|
+
when: backup_files.files | length > 7
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Gravity Platform Database Setup Playbook
|
|
3
|
+
# Creates database tables - uses CREATE TABLE IF NOT EXISTS (safe to run multiple times)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ansible-playbook -i inventory/production.yml playbooks/db-setup.yml
|
|
7
|
+
#
|
|
8
|
+
# Prerequisites:
|
|
9
|
+
# - Core services deployed (install.yml)
|
|
10
|
+
# - DATABASE_URL configured in /opt/gravity/.env
|
|
11
|
+
# - Required extensions enabled in your DB provider:
|
|
12
|
+
# vector, pg_stat_statements
|
|
13
|
+
# (DigitalOcean: Database → Settings → Allowed Extensions)
|
|
14
|
+
|
|
15
|
+
- name: Database Setup
|
|
16
|
+
hosts: all
|
|
17
|
+
become: yes
|
|
18
|
+
|
|
19
|
+
tasks:
|
|
20
|
+
- name: "[1/5] Verify .env exists"
|
|
21
|
+
stat:
|
|
22
|
+
path: /opt/gravity/.env
|
|
23
|
+
register: env_file
|
|
24
|
+
|
|
25
|
+
- name: "[1/5] Fail if .env missing"
|
|
26
|
+
fail:
|
|
27
|
+
msg: ".env file not found at /opt/gravity/.env - run install.yml first"
|
|
28
|
+
when: not env_file.stat.exists
|
|
29
|
+
|
|
30
|
+
- name: "[2/5] Check DATABASE_URL is configured"
|
|
31
|
+
shell: grep -q "^DATABASE_URL=" /opt/gravity/.env
|
|
32
|
+
register: db_url_check
|
|
33
|
+
ignore_errors: yes
|
|
34
|
+
|
|
35
|
+
- name: "[2/5] Fail if DATABASE_URL missing"
|
|
36
|
+
fail:
|
|
37
|
+
msg: "DATABASE_URL not found in .env - configure database connection first"
|
|
38
|
+
when: db_url_check.rc != 0
|
|
39
|
+
|
|
40
|
+
- name: "[3/5] Enable required PostgreSQL extensions"
|
|
41
|
+
command: >
|
|
42
|
+
docker compose exec -T -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse node -e
|
|
43
|
+
"const{Pool}=require('pg');
|
|
44
|
+
const p=new Pool({connectionString:process.env.DATABASE_URL,ssl:{rejectUnauthorized:false}});
|
|
45
|
+
(async()=>{
|
|
46
|
+
const exts=['vector','pg_stat_statements'];
|
|
47
|
+
const results=[];
|
|
48
|
+
for(const ext of exts){
|
|
49
|
+
try{await p.query('CREATE EXTENSION IF NOT EXISTS '+ext);results.push(ext+': OK')}
|
|
50
|
+
catch(e){results.push(ext+': FAILED ('+e.message+')')}
|
|
51
|
+
}
|
|
52
|
+
console.log(results.join('\n'));
|
|
53
|
+
await p.end();
|
|
54
|
+
process.exit(results.some(r=>r.includes('FAILED'))?1:0);
|
|
55
|
+
})()"
|
|
56
|
+
args:
|
|
57
|
+
chdir: /opt/gravity
|
|
58
|
+
register: ext_result
|
|
59
|
+
ignore_errors: yes
|
|
60
|
+
|
|
61
|
+
- name: "[3/5] Extension status"
|
|
62
|
+
debug:
|
|
63
|
+
msg: "{{ ext_result.stdout_lines | default(['No output']) }}"
|
|
64
|
+
|
|
65
|
+
- name: "[3/5] Warn if extensions failed"
|
|
66
|
+
debug:
|
|
67
|
+
msg: |
|
|
68
|
+
⚠️ Some extensions failed to enable. You must enable them in your
|
|
69
|
+
database provider's dashboard BEFORE running this playbook.
|
|
70
|
+
|
|
71
|
+
DigitalOcean: Database → Settings → Allowed Extensions
|
|
72
|
+
Enable: vector, pg_stat_statements
|
|
73
|
+
|
|
74
|
+
See docs/runbooks/02-database.md for details.
|
|
75
|
+
when: ext_result.rc != 0
|
|
76
|
+
|
|
77
|
+
# Apply the .sql migrations with node-pg-migrate — same as `./unoverse db-setup`
|
|
78
|
+
# (scripts/lib/db-setup.sh). The old programmatic `require('./dist/db')` table
|
|
79
|
+
# setup is retired: the engine no longer ships a dist, and .sql migrations under
|
|
80
|
+
# apps/unoverse/engine/migrations are the single source of truth.
|
|
81
|
+
- name: "[4/5] Apply database migrations (node-pg-migrate)"
|
|
82
|
+
shell: |
|
|
83
|
+
docker compose exec -T -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse \
|
|
84
|
+
npx node-pg-migrate up \
|
|
85
|
+
--migrations-dir /app/apps/unoverse/engine/migrations \
|
|
86
|
+
--migration-file-language sql \
|
|
87
|
+
--no-lock
|
|
88
|
+
args:
|
|
89
|
+
chdir: /opt/gravity
|
|
90
|
+
register: migrate_result
|
|
91
|
+
ignore_errors: yes
|
|
92
|
+
|
|
93
|
+
- name: "[4/5] Migration output"
|
|
94
|
+
debug:
|
|
95
|
+
msg: "{{ migrate_result.stdout_lines | default(['No output']) }}"
|
|
96
|
+
|
|
97
|
+
- name: "[5/5] Verify database connectivity"
|
|
98
|
+
uri:
|
|
99
|
+
url: "http://localhost:4101/health"
|
|
100
|
+
status_code: 200
|
|
101
|
+
register: db_check
|
|
102
|
+
retries: 5
|
|
103
|
+
delay: 2
|
|
104
|
+
until: db_check.status == 200
|
|
105
|
+
ignore_errors: yes
|
|
106
|
+
|
|
107
|
+
post_tasks:
|
|
108
|
+
- name: "=== DATABASE MIGRATION SUMMARY ==="
|
|
109
|
+
debug:
|
|
110
|
+
msg: |
|
|
111
|
+
============================================
|
|
112
|
+
DATABASE MIGRATION
|
|
113
|
+
============================================
|
|
114
|
+
Host: {{ inventory_hostname }} ({{ ansible_host }})
|
|
115
|
+
Extensions: {{ 'OK' if ext_result.rc == 0 else 'FAILED — enable in DB provider dashboard' }}
|
|
116
|
+
Migration: {{ 'OK' if migrate_result.rc == 0 else 'FAILED — check logs above' }}
|
|
117
|
+
|
|
118
|
+
If migration failed, check:
|
|
119
|
+
- Extensions enabled in DB provider (vector, pg_stat_statements)
|
|
120
|
+
- DATABASE_URL in /opt/gravity/.env
|
|
121
|
+
- Database is accessible from VM
|
|
122
|
+
- See docs/runbooks/02-database.md for setup instructions
|
|
123
|
+
============================================
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
# DEPLOY IMAGES — the images half of the platform deploy.
|
|
3
|
+
#
|
|
4
|
+
# Pull the latest DOCR images and restart. That's the whole deployment:
|
|
5
|
+
# - platform code (incl. plugin-base, baked in the unoverse image) = the images
|
|
6
|
+
# - marketplace nodes CONVERGE at boot (keep-latest: the shared installed_plugins
|
|
7
|
+
# row is "the newest version anyone chose"; boot reinstalls anything missing
|
|
8
|
+
# or older — see runtime/plugins/startup.ts)
|
|
9
|
+
# - component nodes synthesize from rx/ definitions at boot (no build, ever)
|
|
10
|
+
#
|
|
11
|
+
# The CLI's `unoverse deploy` runs this first, then deploy-packages.yml (the
|
|
12
|
+
# local-source half). Use directly only when images alone changed.
|
|
13
|
+
#
|
|
14
|
+
# Usage (via CLI — recommended):
|
|
15
|
+
# unoverse deploy
|
|
16
|
+
#
|
|
17
|
+
# Usage (direct ansible):
|
|
18
|
+
# ansible-playbook -i inventory playbooks/deploy-a.yml
|
|
19
|
+
|
|
20
|
+
- name: Deploy platform images
|
|
21
|
+
hosts: all
|
|
22
|
+
become: yes
|
|
23
|
+
|
|
24
|
+
vars:
|
|
25
|
+
gravity_dir: /opt/gravity
|
|
26
|
+
|
|
27
|
+
tasks:
|
|
28
|
+
# Sync the compose FIRST so config changes (env vars, ports, new services) ship with
|
|
29
|
+
# a normal `deploy` — not only via `deploy init`. Previously deploy just re-ran the
|
|
30
|
+
# server's existing compose, so a new env var (e.g. MEMORY_SERVICE_URL) silently never
|
|
31
|
+
# reached prod even though the image had the code. `.env` (per-server values) is left
|
|
32
|
+
# untouched — only the canonical compose is refreshed from the repo.
|
|
33
|
+
- name: "[1/4] Sync docker-compose.yml (config travels with the deploy)"
|
|
34
|
+
copy:
|
|
35
|
+
src: "{{ playbook_dir }}/../../docker-compose.yml"
|
|
36
|
+
dest: "{{ gravity_dir }}/docker-compose.yml"
|
|
37
|
+
|
|
38
|
+
- name: "[2/4] Pull latest platform images"
|
|
39
|
+
shell: |
|
|
40
|
+
cd {{ gravity_dir }}
|
|
41
|
+
docker compose pull 2>&1 | tail -8
|
|
42
|
+
register: pull_result
|
|
43
|
+
|
|
44
|
+
- name: "[2/4] Pull output"
|
|
45
|
+
debug:
|
|
46
|
+
msg: "{{ pull_result.stdout_lines[-8:] | default(['done']) }}"
|
|
47
|
+
|
|
48
|
+
- name: "[3/4] Restart on the new images (marketplace converges at boot)"
|
|
49
|
+
shell: |
|
|
50
|
+
cd {{ gravity_dir }}
|
|
51
|
+
docker compose up -d 2>&1 | tail -6
|
|
52
|
+
register: up_result
|
|
53
|
+
|
|
54
|
+
- name: "[4/4] Service status"
|
|
55
|
+
shell: |
|
|
56
|
+
cd {{ gravity_dir }}
|
|
57
|
+
docker compose ps --format '{{ '{{' }}.Name{{ '}}' }}: {{ '{{' }}.Status{{ '}}' }}' 2>&1
|
|
58
|
+
register: status_result
|
|
59
|
+
|
|
60
|
+
- name: "=== IMAGES DEPLOYED ==="
|
|
61
|
+
debug:
|
|
62
|
+
msg: |
|
|
63
|
+
============================================
|
|
64
|
+
PLATFORM IMAGES DEPLOYED
|
|
65
|
+
============================================
|
|
66
|
+
Host: {{ inventory_hostname }} ({{ ansible_host }})
|
|
67
|
+
{{ status_result.stdout }}
|
|
68
|
+
Marketplace nodes converge to the recorded
|
|
69
|
+
versions at boot; component nodes synthesize
|
|
70
|
+
from rx/ definitions.
|
|
71
|
+
============================================
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Gravity Platform VM Hardening Playbook
|
|
3
|
+
# Enterprise security hardening for production VMs
|
|
4
|
+
|
|
5
|
+
- name: Harden VM for Enterprise Deployment
|
|
6
|
+
hosts: all
|
|
7
|
+
become: yes
|
|
8
|
+
|
|
9
|
+
vars:
|
|
10
|
+
# SSH hardening.
|
|
11
|
+
# PermitRootLogin MUST be prohibit-password, never "no": unoverse deploy and
|
|
12
|
+
# unoverse update SSH in as root (DEPLOY_USER=root on DO). "no" locks every
|
|
13
|
+
# future deploy out of the box after the first harden run; prohibit-password
|
|
14
|
+
# keeps key-based root working and blocks only password logins — which is
|
|
15
|
+
# the attack fail2ban and this hardening actually defend against.
|
|
16
|
+
ssh_port: 22
|
|
17
|
+
ssh_permit_root_login: "prohibit-password"
|
|
18
|
+
ssh_password_authentication: "no"
|
|
19
|
+
|
|
20
|
+
# NO firewall vars here: the firewall is the ground's (cloud firewall / SGs,
|
|
21
|
+
# owned by infra/ Terraform). This playbook must never touch app ports —
|
|
22
|
+
# 4105 (API/MCP: Canvas, Studio, Claude, ChatGPT) and 3001 stay governed by
|
|
23
|
+
# the load balancer and admin_cidr, so hardening can't cut clients off.
|
|
24
|
+
|
|
25
|
+
tasks:
|
|
26
|
+
# =========================================================================
|
|
27
|
+
# SYSTEM UPDATES
|
|
28
|
+
# =========================================================================
|
|
29
|
+
- name: Update all packages (Debian/Ubuntu)
|
|
30
|
+
apt:
|
|
31
|
+
upgrade: safe
|
|
32
|
+
update_cache: yes
|
|
33
|
+
cache_valid_time: 3600
|
|
34
|
+
when: ansible_facts['os_family'] == "Debian"
|
|
35
|
+
|
|
36
|
+
- name: Update all packages (RHEL/Amazon Linux)
|
|
37
|
+
dnf:
|
|
38
|
+
name: "*"
|
|
39
|
+
state: latest
|
|
40
|
+
security: yes
|
|
41
|
+
when: ansible_facts['os_family'] == "RedHat"
|
|
42
|
+
|
|
43
|
+
- name: Install security packages (Debian/Ubuntu)
|
|
44
|
+
apt:
|
|
45
|
+
name:
|
|
46
|
+
- fail2ban
|
|
47
|
+
- unattended-upgrades
|
|
48
|
+
- logrotate
|
|
49
|
+
state: present
|
|
50
|
+
when: ansible_facts['os_family'] == "Debian"
|
|
51
|
+
|
|
52
|
+
- name: Install security packages (RHEL/Amazon Linux)
|
|
53
|
+
dnf:
|
|
54
|
+
name:
|
|
55
|
+
- fail2ban
|
|
56
|
+
- dnf-automatic
|
|
57
|
+
- logrotate
|
|
58
|
+
- audit
|
|
59
|
+
state: present
|
|
60
|
+
when: ansible_facts['os_family'] == "RedHat"
|
|
61
|
+
|
|
62
|
+
# =========================================================================
|
|
63
|
+
# SSH HARDENING
|
|
64
|
+
# =========================================================================
|
|
65
|
+
- name: Configure SSH - Disable root login
|
|
66
|
+
lineinfile:
|
|
67
|
+
path: /etc/ssh/sshd_config
|
|
68
|
+
regexp: "^#?PermitRootLogin"
|
|
69
|
+
line: "PermitRootLogin {{ ssh_permit_root_login }}"
|
|
70
|
+
notify: Restart SSH
|
|
71
|
+
|
|
72
|
+
- name: Configure SSH - Disable password authentication
|
|
73
|
+
lineinfile:
|
|
74
|
+
path: /etc/ssh/sshd_config
|
|
75
|
+
regexp: "^#?PasswordAuthentication"
|
|
76
|
+
line: "PasswordAuthentication {{ ssh_password_authentication }}"
|
|
77
|
+
notify: Restart SSH
|
|
78
|
+
|
|
79
|
+
- name: Configure SSH - Use only SSH protocol 2
|
|
80
|
+
lineinfile:
|
|
81
|
+
path: /etc/ssh/sshd_config
|
|
82
|
+
regexp: "^#?Protocol"
|
|
83
|
+
line: "Protocol 2"
|
|
84
|
+
notify: Restart SSH
|
|
85
|
+
|
|
86
|
+
- name: Configure SSH - Set login grace time
|
|
87
|
+
lineinfile:
|
|
88
|
+
path: /etc/ssh/sshd_config
|
|
89
|
+
regexp: "^#?LoginGraceTime"
|
|
90
|
+
line: "LoginGraceTime 60"
|
|
91
|
+
notify: Restart SSH
|
|
92
|
+
|
|
93
|
+
- name: Configure SSH - Limit max auth tries
|
|
94
|
+
lineinfile:
|
|
95
|
+
path: /etc/ssh/sshd_config
|
|
96
|
+
regexp: "^#?MaxAuthTries"
|
|
97
|
+
line: "MaxAuthTries 3"
|
|
98
|
+
notify: Restart SSH
|
|
99
|
+
|
|
100
|
+
# =========================================================================
|
|
101
|
+
# FIREWALL: NONE HERE (moved to the ground, 2026-07-28)
|
|
102
|
+
# =========================================================================
|
|
103
|
+
# The cloud firewall (DO cloud firewall / AWS security groups, owned by
|
|
104
|
+
# infra/ Terraform) is the boundary. Host ufw/firewalld was removed: Docker's
|
|
105
|
+
# iptables rules BYPASS ufw for published ports, so it protected nothing —
|
|
106
|
+
# the ground firewall structurally can't be bypassed (traffic never reaches
|
|
107
|
+
# the box). On-prem: firewall at the customer's network layer, not here.
|
|
108
|
+
|
|
109
|
+
- name: Configure fail2ban for SSH
|
|
110
|
+
copy:
|
|
111
|
+
dest: /etc/fail2ban/jail.local
|
|
112
|
+
content: |
|
|
113
|
+
[DEFAULT]
|
|
114
|
+
bantime = 3600
|
|
115
|
+
findtime = 600
|
|
116
|
+
maxretry = 3
|
|
117
|
+
|
|
118
|
+
[sshd]
|
|
119
|
+
enabled = true
|
|
120
|
+
port = {{ ssh_port }}
|
|
121
|
+
filter = sshd
|
|
122
|
+
logpath = /var/log/auth.log
|
|
123
|
+
maxretry = 3
|
|
124
|
+
notify: Restart fail2ban
|
|
125
|
+
|
|
126
|
+
# =========================================================================
|
|
127
|
+
# AUTOMATIC SECURITY UPDATES
|
|
128
|
+
# =========================================================================
|
|
129
|
+
- name: Enable automatic security updates (Debian/Ubuntu)
|
|
130
|
+
copy:
|
|
131
|
+
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
|
132
|
+
content: |
|
|
133
|
+
APT::Periodic::Update-Package-Lists "1";
|
|
134
|
+
APT::Periodic::Unattended-Upgrade "1";
|
|
135
|
+
APT::Periodic::AutocleanInterval "7";
|
|
136
|
+
when: ansible_facts['os_family'] == "Debian"
|
|
137
|
+
|
|
138
|
+
- name: Enable automatic security updates (RHEL/Amazon Linux)
|
|
139
|
+
service:
|
|
140
|
+
name: dnf-automatic.timer
|
|
141
|
+
state: started
|
|
142
|
+
enabled: yes
|
|
143
|
+
when: ansible_facts['os_family'] == "RedHat"
|
|
144
|
+
|
|
145
|
+
# =========================================================================
|
|
146
|
+
# SYSTEM LIMITS
|
|
147
|
+
# =========================================================================
|
|
148
|
+
- name: Set file descriptor limits
|
|
149
|
+
copy:
|
|
150
|
+
dest: /etc/security/limits.d/gravity.conf
|
|
151
|
+
content: |
|
|
152
|
+
# Gravity Platform limits
|
|
153
|
+
* soft nofile 65535
|
|
154
|
+
* hard nofile 65535
|
|
155
|
+
root soft nofile 65535
|
|
156
|
+
root hard nofile 65535
|
|
157
|
+
|
|
158
|
+
# =========================================================================
|
|
159
|
+
# DOCKER HARDENING
|
|
160
|
+
# =========================================================================
|
|
161
|
+
- name: Create Docker daemon config
|
|
162
|
+
copy:
|
|
163
|
+
dest: /etc/docker/daemon.json
|
|
164
|
+
content: |
|
|
165
|
+
{
|
|
166
|
+
"log-driver": "json-file",
|
|
167
|
+
"log-opts": {
|
|
168
|
+
"max-size": "100m",
|
|
169
|
+
"max-file": "3"
|
|
170
|
+
},
|
|
171
|
+
"live-restore": true,
|
|
172
|
+
"userland-proxy": false
|
|
173
|
+
}
|
|
174
|
+
notify: Restart Docker
|
|
175
|
+
|
|
176
|
+
# =========================================================================
|
|
177
|
+
# AUDIT LOGGING
|
|
178
|
+
# =========================================================================
|
|
179
|
+
- name: Install auditd (Debian/Ubuntu)
|
|
180
|
+
apt:
|
|
181
|
+
name: auditd
|
|
182
|
+
state: present
|
|
183
|
+
when: ansible_facts['os_family'] == "Debian"
|
|
184
|
+
|
|
185
|
+
- name: Ensure auditd running (RHEL/Amazon Linux)
|
|
186
|
+
service:
|
|
187
|
+
name: auditd
|
|
188
|
+
state: started
|
|
189
|
+
enabled: yes
|
|
190
|
+
when: ansible_facts['os_family'] == "RedHat"
|
|
191
|
+
|
|
192
|
+
- name: Configure audit rules for Docker
|
|
193
|
+
copy:
|
|
194
|
+
dest: /etc/audit/rules.d/docker.rules
|
|
195
|
+
content: |
|
|
196
|
+
# Docker daemon
|
|
197
|
+
-w /usr/bin/docker -p wa -k docker
|
|
198
|
+
-w /var/lib/docker -p wa -k docker
|
|
199
|
+
-w /etc/docker -p wa -k docker
|
|
200
|
+
-w /lib/systemd/system/docker.service -p wa -k docker
|
|
201
|
+
-w /etc/docker/daemon.json -p wa -k docker
|
|
202
|
+
notify: Restart auditd
|
|
203
|
+
|
|
204
|
+
handlers:
|
|
205
|
+
- name: Restart SSH
|
|
206
|
+
service:
|
|
207
|
+
name: ssh
|
|
208
|
+
state: restarted
|
|
209
|
+
|
|
210
|
+
- name: Restart fail2ban
|
|
211
|
+
service:
|
|
212
|
+
name: fail2ban
|
|
213
|
+
state: restarted
|
|
214
|
+
|
|
215
|
+
- name: Restart Docker
|
|
216
|
+
service:
|
|
217
|
+
name: docker
|
|
218
|
+
state: restarted
|
|
219
|
+
|
|
220
|
+
- name: Restart auditd
|
|
221
|
+
service:
|
|
222
|
+
name: auditd
|
|
223
|
+
state: restarted
|