feat(setup): Ubuntu 26.04 LTS support + cert autofirmato + 3 file .example

Modifiche di compatibilita' e miglioramenti UX dopo analisi del 12/05/26
del codice argos main repo vs argos-setup.

1. Multi-version Ubuntu (24.04 e 26.04 LTS):
   - first-setup.sh: SUPPORTED_VERSIONS array per check
   - Aggiunto openssl esplicito alle dipendenze apt
   - README aggiornato con compatibility matrix
   - ARGOS code (Python 3.14-ready: no distutils/imp/pkg_resources)
     gia' compatibile, requirements.txt pin gia' build 2026

2. Certificato SSL autofirmato come 3a opzione:
   - UI: bottone '🔐 Autofirmato' nel Tab Rete & SSL
   - Backend: openssl req -x509 -newkey rsa:4096 -days 3650
   - SAN dinamica: tutti i DNS (hostname + aliases) + IP server
   - Subject: C=IT, O=<cliente_full>, OU=ARGOS SOC, CN=<hostname>
   - Warning UI prominente sulla non-attendibilita' browser
   - Utile per installazioni LAN/dev senza DNS pubblico

3. Rimozione campi M365:
   - sp_tenant (SharePoint tenant) rimosso da Tab Cliente
   - sharepoint_tenant non piu' nel argos.json generato
   - M365/Entra ID configurabile dall'UI Integrazioni post-install

4. Textarea ai_context opzionale nel Tab Cliente:
   - Iniettato nei prompt AI per contestualizzare il cliente
   - Hint con esempio ASREM
   - Salvato in argos.json -> cliente.ai_context (letto da config.py)

5. Copia di 3 file .example che il setup non gestiva:
   - automations.json (config feed TI sources + cron daemon)
   - siem_integrations.json (catalogo SIEM Integration Builder)
   - subnet_registry.json (mapping sede/reparto da subnet)
   Necessari dopo i lavori marzo-maggio 2026.

6. Rimozione gen_config.py legacy:
   - Schema obsoleto (manca ai_context, console_url, network,
     vendor_heartbeat)
   - Non piu' richiamato da nessuno (verificato con grep)
   - Sostituito completamente da setup_server.py inline

7. README aggiornato:
   - Sezione 'Opzioni certificato SSL' con 3 modalita'
   - Requisiti: Ubuntu 24.04 LTS o 26.04 LTS
   - Tab 2 menziona contesto AI opzionale
This commit is contained in:
tecnotel 2026-05-12 11:20:37 +02:00
parent 6424c784f9
commit a68324725f
4 changed files with 130 additions and 14 deletions

View File

@ -6,7 +6,7 @@ Repository pubblico contenente l'installer di prima fase e il Web Setup Wizard d
## Installazione rapida (one-liner) ## Installazione rapida (one-liner)
Su una macchina Ubuntu 24.04 LTS vergine, esegui: Su una macchina Ubuntu 24.04 o 26.04 LTS vergine, esegui:
```bash ```bash
curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash
@ -33,25 +33,32 @@ sudo bash first-setup.sh
| `first-setup.sh` | Installer ambiente base: sistema, utenti, firewall, nginx temp, avvia wizard web | | `first-setup.sh` | Installer ambiente base: sistema, utenti, firewall, nginx temp, avvia wizard web |
| `setup_server.py` | Backend Python del Web Installer (porta 8888, self-contained) | | `setup_server.py` | Backend Python del Web Installer (porta 8888, self-contained) |
| `setup.html` | Frontend del Web Installer — wizard in 6 step | | `setup.html` | Frontend del Web Installer — wizard in 6 step |
| `gen_config.py` | Helper per generazione iniziale di `argos.json` |
## Flusso d'installazione ## Flusso d'installazione
1. Il cliente esegue il bootstrap (o il clone manuale + first-setup.sh). 1. Il cliente esegue il bootstrap (o il clone manuale + first-setup.sh).
2. `first-setup.sh` predispone l'ambiente base Ubuntu 24.04 (pacchetti, utente argos, cartelle, firewall) e avvia il Web Installer su `http://IP:8888`. 2. `first-setup.sh` predispone l'ambiente base Ubuntu (pacchetti, utente argos, cartelle, firewall) e avvia il Web Installer su `http://IP:8888`.
3. Il cliente apre il browser e segue il wizard in 6 step: 3. Il cliente apre il browser e segue il wizard in 6 step:
1. **Licenza ARGOS** — il wizard mostra il `machine_id` di questo server. Il cliente lo invia a Tecnotel, riceve `license.json`, lo carica nel wizard. 1. **Licenza ARGOS** — il wizard mostra il `machine_id` di questo server. Il cliente lo invia a Tecnotel, riceve `license.json`, lo carica nel wizard.
2. **Informazioni cliente** (nome, dominio, logo) 2. **Informazioni cliente** (nome, dominio, logo, contesto AI opzionale)
3. **Rete & SSL** (hostname, certificato) 3. **Rete & SSL** (hostname, certificato: Let's Encrypt / caricamento / autofirmato)
4. **SIEM** (OpenSearch) 4. **SIEM** (OpenSearch)
5. **Utente admin** 5. **Utente admin**
6. **Riepilogo & Installazione** — il wizard clona il repository privato `tecnotel/argos` usando il token della licenza, crea virtualenv, configura servizi e avvia ARGOS SOC. 6. **Riepilogo & Installazione** — il wizard clona il repository privato `tecnotel/argos` usando il token della licenza, crea virtualenv, configura servizi e avvia ARGOS SOC.
Al termine il Web Installer viene disattivato automaticamente e la porta 8888 chiusa. Al termine il Web Installer viene disattivato automaticamente e la porta 8888 chiusa.
## Opzioni certificato SSL
Il wizard offre tre modalità per il certificato:
- **🔒 Let's Encrypt** — automatico e gratuito. Richiede DNS pubblico già puntato al server.
- **📄 Certificato esistente** — caricamento di file `.crt` e `.key` esistenti (es. cert wildcard aziendale, cert di CA interna).
- **🔐 Autofirmato** — generato localmente con RSA 4096 e validità 10 anni. Utile per installazioni in LAN / dev / ambienti isolati senza DNS pubblico. ⚠️ I browser segnaleranno il certificato come non attendibile.
## Requisiti ## Requisiti
- Ubuntu 24.04 LTS (verificato dallo script) - Ubuntu **24.04 LTS** o **26.04 LTS** (verificato dallo script)
- Accesso root (sudo) - Accesso root (sudo)
- Connessione internet verso `argos-update.tecnotelsrl.com` - Connessione internet verso `argos-update.tecnotelsrl.com`
- Una licenza ARGOS valida emessa da Tecnotel per il `machine_id` del server - Una licenza ARGOS valida emessa da Tecnotel per il `machine_id` del server
@ -63,3 +70,4 @@ Al termine il Web Installer viene disattivato automaticamente e la porta 8888 ch
## Versioning ## Versioning
Questo repository segue lo stesso schema di versioning di `argos` (SemVer tag `vX.Y.Z`). Questo repository segue lo stesso schema di versioning di `argos` (SemVer tag `vX.Y.Z`).

View File

@ -16,7 +16,14 @@ section() { echo -e "\n${BLUE}════════════════
[[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash first-setup.sh" [[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash first-setup.sh"
. /etc/os-release . /etc/os-release
[[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS" SUPPORTED_VERSIONS=("24.04" "26.04")
VERSION_OK=0
for v in "${SUPPORTED_VERSIONS[@]}"; do
[[ "$VERSION_ID" == "$v" ]] && VERSION_OK=1
done
[[ "$ID" != "ubuntu" || $VERSION_OK -eq 0 ]] && \
error "Richiesto Ubuntu 24.04 o 26.04 LTS (rilevato: $ID $VERSION_ID)"
info "Sistema rilevato: Ubuntu $VERSION_ID LTS"
echo "" echo ""
echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}" echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}"
@ -39,7 +46,7 @@ apt-get install -y -qq \
ufw fail2ban \ ufw fail2ban \
build-essential libssl-dev libffi-dev python3-dev \ build-essential libssl-dev libffi-dev python3-dev \
sqlite3 net-tools dnsutils lsof tcpdump nmap \ sqlite3 net-tools dnsutils lsof tcpdump nmap \
ca-certificates gnupg apt-transport-https ca-certificates gnupg apt-transport-https openssl
success "Pacchetti sistema installati" success "Pacchetti sistema installati"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════

View File

@ -172,7 +172,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div><label class="form-label">Tipo organizzazione</label><select class="form-select" id="cliente_type"><option value="enterprise">Azienda / Enterprise</option><option value="healthcare">Sanità / Healthcare</option><option value="pa">Pubblica Amministrazione</option><option value="education">Istruzione</option></select></div> <div><label class="form-label">Tipo organizzazione</label><select class="form-select" id="cliente_type"><option value="enterprise">Azienda / Enterprise</option><option value="healthcare">Sanità / Healthcare</option><option value="pa">Pubblica Amministrazione</option><option value="education">Istruzione</option></select></div>
<div class="form-full"><label class="form-label">Nome completo</label><input class="form-input" id="cliente_full" placeholder="es. Nome Azienda Srl"></div> <div class="form-full"><label class="form-label">Nome completo</label><input class="form-input" id="cliente_full" placeholder="es. Nome Azienda Srl"></div>
<div><label class="form-label">Dominio email <span>*</span></label><input class="form-input" id="cliente_domain" placeholder="es. azienda.it"><div class="form-hint">Dominio utenti (es. nome@azienda.it)</div></div> <div><label class="form-label">Dominio email <span>*</span></label><input class="form-input" id="cliente_domain" placeholder="es. azienda.it"><div class="form-hint">Dominio utenti (es. nome@azienda.it)</div></div>
<div><label class="form-label">SharePoint tenant</label><input class="form-input" id="sp_tenant" placeholder="es. nomeazienda"><div class="form-hint">Solo se Microsoft 365. Vuoto se non usato.</div></div> <div class="form-full"><label class="form-label">Contesto AI <span style="color:var(--text4);font-weight:400">(opzionale)</span></label><textarea class="form-input" id="ai_context" rows="3" placeholder="es. ASREM = Azienda Sanitaria Regionale Molise, 17 sedi, 2000+ endpoint, infrastruttura mista Cisco/Fortinet, normative NIS2/GDPR/AgID."></textarea><div class="form-hint">3-4 frasi sul cliente per contestualizzare i prompt AI. Iniettato in ogni richiesta AI per sintesi più accurate.</div></div>
<div class="sec-div">Logo cliente (per i PDF report)</div> <div class="sec-div">Logo cliente (per i PDF report)</div>
<div class="upload-area form-full" id="logo-drop" ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')" ondrop="handleLogoDrop(event)" onclick="document.getElementById('logo-input').click()"> <div class="upload-area form-full" id="logo-drop" ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')" ondrop="handleLogoDrop(event)" onclick="document.getElementById('logo-input').click()">
<input type="file" id="logo-input" accept="image/*" style="display:none" onchange="handleLogoFile(this)"> <input type="file" id="logo-input" accept="image/*" style="display:none" onchange="handleLogoFile(this)">
@ -196,6 +196,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div class="ssl-toggle"> <div class="ssl-toggle">
<div class="ssl-btn active" id="ssl-le" onclick="setSsl('letsencrypt')">🔒 Let's Encrypt<small>Automatico e gratuito</small></div> <div class="ssl-btn active" id="ssl-le" onclick="setSsl('letsencrypt')">🔒 Let's Encrypt<small>Automatico e gratuito</small></div>
<div class="ssl-btn" id="ssl-manual" onclick="setSsl('manual')">📄 Certificato esistente<small>Carica .crt e .key</small></div> <div class="ssl-btn" id="ssl-manual" onclick="setSsl('manual')">📄 Certificato esistente<small>Carica .crt e .key</small></div>
<div class="ssl-btn" id="ssl-selfsigned" onclick="setSsl('selfsigned')">🔐 Autofirmato<small>LAN / dev — 10 anni</small></div>
</div> </div>
<div id="le-block" class="form-full"><label class="form-label">Email amministratore <span>*</span></label><input class="form-input" id="admin_email" placeholder="admin@tecnotelsrl.com"><div class="form-hint">Per notifiche di scadenza certificato</div></div> <div id="le-block" class="form-full"><label class="form-label">Email amministratore <span>*</span></label><input class="form-input" id="admin_email" placeholder="admin@tecnotelsrl.com"><div class="form-hint">Per notifiche di scadenza certificato</div></div>
<div id="manual-block" class="form-full" style="display:none"> <div id="manual-block" class="form-full" style="display:none">
@ -204,6 +205,17 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div><label class="form-label">File .key <span>*</span></label><input type="file" class="form-input" accept=".key,.pem" onchange="uploadSsl(this,'key')"><div class="form-hint" id="key-status">Nessun file selezionato</div></div> <div><label class="form-label">File .key <span>*</span></label><input type="file" class="form-input" accept=".key,.pem" onchange="uploadSsl(this,'key')"><div class="form-hint" id="key-status">Nessun file selezionato</div></div>
</div> </div>
</div> </div>
<div id="selfsigned-block" class="form-full" style="display:none">
<div style="padding:12px;background:rgba(217,119,6,0.08);border:1px solid rgba(217,119,6,0.3);border-radius:6px;font-size:13px;color:var(--text2);line-height:1.5">
<strong style="color:var(--warn)">⚠️ Attenzione — certificato autofirmato</strong><br>
Il certificato verrà generato localmente con RSA 4096 e validità 10 anni.
I browser segnaleranno il sito come <em>non attendibile</em> al primo accesso —
sarà necessario aggiungere un'eccezione manualmente.<br>
<strong>Usare solo per installazioni LAN, test o ambienti isolati.</strong>
Per produzione su Internet pubblico preferire <em>Let's Encrypt</em> o un
certificato firmato da CA aziendale.
</div>
</div>
</div> </div>
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(1)">← Indietro</button><button class="btn btn-brand" onclick="goTab(3)">Avanti →</button></div> <div class="nav-row"><button class="btn btn-ghost" onclick="goTab(1)">← Indietro</button><button class="btn btn-brand" onclick="goTab(3)">Avanti →</button></div>
</div> </div>
@ -400,8 +412,10 @@ function setSsl(mode) {
sslMode = mode; sslMode = mode;
document.getElementById('ssl-le').classList.toggle('active', mode === 'letsencrypt'); document.getElementById('ssl-le').classList.toggle('active', mode === 'letsencrypt');
document.getElementById('ssl-manual').classList.toggle('active', mode === 'manual'); document.getElementById('ssl-manual').classList.toggle('active', mode === 'manual');
document.getElementById('ssl-selfsigned').classList.toggle('active', mode === 'selfsigned');
document.getElementById('le-block').style.display = mode === 'letsencrypt' ? 'block' : 'none'; document.getElementById('le-block').style.display = mode === 'letsencrypt' ? 'block' : 'none';
document.getElementById('manual-block').style.display = mode === 'manual' ? 'block' : 'none'; document.getElementById('manual-block').style.display = mode === 'manual' ? 'block' : 'none';
document.getElementById('selfsigned-block').style.display = mode === 'selfsigned' ? 'block' : 'none';
} }
async function uploadSsl(input, type) { async function uploadSsl(input, type) {
@ -438,7 +452,8 @@ function collectData() {
return { return {
cliente_name: g('cliente_name'), cliente_full: g('cliente_full'), cliente_name: g('cliente_name'), cliente_full: g('cliente_full'),
cliente_domain: g('cliente_domain'), cliente_type: g('cliente_type'), cliente_domain: g('cliente_domain'), cliente_type: g('cliente_type'),
sp_tenant: g('sp_tenant'), domain: g('domain'), aliases: g('aliases'), ai_context: g('ai_context'),
domain: g('domain'), aliases: g('aliases'),
ssl_mode: sslMode, admin_email: g('admin_email'), ssl_mode: sslMode, admin_email: g('admin_email'),
os_url: g('os_url'), os_user: g('os_user') || 'admin', os_pass: g('os_pass'), os_url: g('os_url'), os_user: g('os_user') || 'admin', os_pass: g('os_pass'),
admin_username: g('admin_username') || 'admin', admin_username: g('admin_username') || 'admin',
@ -460,13 +475,19 @@ function buildSummary() {
const row = (l, v) => '<div class="summary-row"><div class="summary-label">' + l + '</div><div class="summary-value ' + (v ? '' : 'empty') + '">' + (v || 'non configurato') + '</div></div>'; const row = (l, v) => '<div class="summary-row"><div class="summary-label">' + l + '</div><div class="summary-value ' + (v ? '' : 'empty') + '">' + (v || 'non configurato') + '</div></div>';
const sslLabel = sslMode === 'letsencrypt' ? "Let's Encrypt (automatico)"
: sslMode === 'manual' ? "Certificato esistente caricato"
: sslMode === 'selfsigned' ? "Autofirmato (RSA 4096, 10 anni)"
: "Non configurato";
document.getElementById('summary-box').innerHTML = '<div class="summary-table">' + document.getElementById('summary-box').innerHTML = '<div class="summary-table">' +
'<div class="summary-section">Cliente</div>' + '<div class="summary-section">Cliente</div>' +
row('Nome', d.cliente_name) + row('Nome completo', d.cliente_full) + row('Nome', d.cliente_name) + row('Nome completo', d.cliente_full) +
row('Dominio email', d.cliente_domain) + row('Tipo', d.cliente_type) + row('Dominio email', d.cliente_domain) + row('Tipo', d.cliente_type) +
(d.ai_context ? row('Contesto AI', d.ai_context.substring(0, 80) + (d.ai_context.length > 80 ? '…' : '')) : '') +
'<div class="summary-section">Rete & SSL</div>' + '<div class="summary-section">Rete & SSL</div>' +
row('Hostname', d.domain) + row('Alias', d.aliases) + row('Hostname', d.domain) + row('Alias', d.aliases) +
row('SSL', sslMode === 'letsencrypt' ? "Let's Encrypt" : 'Certificato manuale') + row('SSL', sslLabel) +
'<div class="summary-section">SIEM</div>' + '<div class="summary-section">SIEM</div>' +
row('OpenSearch URL', d.os_url) + row('Username', d.os_user) + row('OpenSearch URL', d.os_url) + row('Username', d.os_user) +
'<div class="summary-section">Utente admin</div>' + '<div class="summary-section">Utente admin</div>' +

View File

@ -177,8 +177,7 @@ def generate_argos_json(data):
"full_name": data.get("cliente_full", ""), "full_name": data.get("cliente_full", ""),
"domain": data.get("cliente_domain", ""), "domain": data.get("cliente_domain", ""),
"type": data.get("cliente_type", "enterprise"), "type": data.get("cliente_type", "enterprise"),
"ai_context": "", "ai_context": data.get("ai_context", ""),
"sharepoint_tenant": "",
"console_url": "" "console_url": ""
}, },
"network": { "network": {
@ -364,6 +363,19 @@ def install(data):
chown(CONFIG_DIR / "modules.json") chown(CONFIG_DIR / "modules.json")
log("modules.json copiato") log("modules.json copiato")
# 5b. File .example aggiuntivi (config sezioni recenti)
# - automations.json: config feed TI sources + cron daemon TI
# - siem_integrations.json: catalogo SIEM Integration Builder
# - subnet_registry.json: mapping sede/reparto da subnet
for stem in ("automations", "siem_integrations", "subnet_registry"):
src = APP_DIR / f"config/{stem}.json.example"
dst = CONFIG_DIR / f"{stem}.json"
if src.exists() and not dst.exists():
shutil.copy(src, dst)
os.chmod(dst, 0o640)
chown(dst)
log(f"{stem}.json copiato da template")
# 6. Logo cliente # 6. Logo cliente
logo_src = SETUP_DIR / "logo_cliente.png" logo_src = SETUP_DIR / "logo_cliente.png"
if logo_src.exists(): if logo_src.exists():
@ -413,6 +425,74 @@ def install(data):
ssl_crt = str(CERTS_DIR / "fullchain.pem") ssl_crt = str(CERTS_DIR / "fullchain.pem")
ssl_key = str(CERTS_DIR / "privkey.pem") ssl_key = str(CERTS_DIR / "privkey.pem")
log("Certificato SSL manuale copiato") log("Certificato SSL manuale copiato")
elif ssl_mode == "selfsigned":
# Cert autofirmato — utile per installazioni LAN/dev senza DNS
# pubblico. Validita' 10 anni, RSA 4096, SAN da hostname+alias.
log("Generazione certificato autofirmato (RSA 4096, validita' 10 anni)")
CERTS_DIR.mkdir(parents=True, exist_ok=True)
crt_path = CERTS_DIR / "fullchain.pem"
key_path = CERTS_DIR / "privkey.pem"
cnf_path = CERTS_DIR / "openssl-selfsigned.cnf"
# Build subjectAltName list: tutti i nomi DNS + IP server
san_dns = [n for n in all_names.split() if n]
try:
server_ip = subprocess.check_output(
["hostname", "-I"], text=True
).strip().split()[0]
except Exception:
server_ip = ""
san_lines = "\n".join(f"DNS.{i+1} = {n}" for i, n in enumerate(san_dns))
if server_ip:
san_lines += f"\nIP.1 = {server_ip}"
# Subject info: cliente_full come O, domain come CN
client_full = data.get("cliente_full") or data.get("cliente_name") or "ARGOS"
cn = domain or "argos.local"
cnf = f"""[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
x509_extensions = v3_ext
[dn]
C = IT
O = {client_full}
OU = ARGOS SOC
CN = {cn}
[req_ext]
subjectAltName = @alt_names
[v3_ext]
subjectAltName = @alt_names
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[alt_names]
{san_lines}
"""
cnf_path.write_text(cnf)
# Genero la chiave e il cert in un solo passo
run(
f"openssl req -x509 -nodes -days 3650 -newkey rsa:4096 "
f"-keyout {key_path} -out {crt_path} -config {cnf_path}"
)
os.chmod(key_path, 0o600)
os.chmod(crt_path, 0o644)
chown(key_path)
chown(crt_path)
ssl_crt = str(crt_path)
ssl_key = str(key_path)
log(f"Cert autofirmato generato (CN={cn}, SAN: {len(san_dns)} DNS"
f"{' + 1 IP' if server_ip else ''})")
log("⚠️ ATTENZIONE: i browser segnaleranno il cert come non attendibile.")
log(" Usare solo per LAN/test. Per produzione: Let's Encrypt o cert CA.")
else: else:
_write_nginx_http(all_names) _write_nginx_http(all_names)
run("nginx -t && systemctl restart nginx") run("nginx -t && systemctl restart nginx")