Skip to content

Commit eacc173

Browse files
committed
feat(forge): chain mios-forge admin creation through build-mios.ps1 + install.env + firstboot service
End-to-end chain so the user identity defined in mios-bootstrap.git/ mios.toml propagates all the way to the Forgejo admin account on the deployed image: mios-bootstrap/mios.toml | v build-mios.ps1 (Windows orchestrator) interactive prompts build-mios.sh (bootstrap Linux installer) interactive prompts | Read-Timed / prompt_default with 3-min auto-accept | defaulting to MIOS_LINUX_USER and <user>@<host>.local v /etc/mios/install.env (mode 0640 in the imported WSL2 distro) | written by tools/lib/install-env.ps1 (Windows side) and | by build-mios.sh's PROFILE_FILE writer (Linux side); both | emit MIOS_FORGE_ADMIN_USER, MIOS_FORGE_ADMIN_EMAIL, and | MIOS_FORGE_ADMIN_PASSWORD= (empty -> generated below) v mios-forge.service starts at boot v mios-forge-firstboot.service (oneshot, RemainAfterExit=yes) | After=mios-forge.service; runs once per fresh deployment; | ConditionPathExists guards on install.env presence and on | absence of the .firstboot-done sentinel. v /usr/libexec/mios/forge-firstboot.sh | - waits up to 300s for Forgejo's /api/v1/version probe | - if MIOS_FORGE_ADMIN_PASSWORD empty, generates a 24-byte | URL-safe random pwd, writes /etc/mios/forge/admin-password | (root-owned, mode 0600); operator reads via 'sudo cat' | - 'podman exec mios-forge forgejo admin user create | --admin --must-change-password=true' against the values | from install.env | - drops a sentinel at /var/lib/mios/forge/.firstboot-done | so re-boots are no-ops v Operator runs: git remote add origin http://localhost:3000/<user>/<repo>.git git push origin main NEW FILES (mios.git) usr/libexec/mios/forge-firstboot.sh (executable; bash -n clean) usr/lib/systemd/system/mios-forge-firstboot.service (oneshot; ProtectSystem= strict; ReadWritePaths scoped to forge dirs only) UPDATED FILES (mios.git) tools/lib/install-env.ps1 Write-MiosInstallEnv signature gains -ForgeAdminUser and -ForgeAdminEmail (both default to the linux identity if omitted). Three new lines emitted into install.env: MIOS_FORGE_ADMIN_USER, MIOS_FORGE_ADMIN_EMAIL, MIOS_FORGE_ADMIN_PASSWORD= (empty -> firstboot generates). build-mios.ps1 - Phase-0 custom workflow now prompts for forge admin user + email via Read-Timed (3-min auto-accept, defaulting to the linux $U and $U@$HostIn.local). - Non-custom workflow takes the same defaults silently. - WSL2 deployment branch passes $ForgeAdmin / $ForgeEmail through Write-MiosInstallEnv. - Phase-5 build summary prints the forge URL, admin user/email, how to read the generated password, and a 'git remote add' one-liner so the operator has the locally-hosted .git=./ flow at their fingertips. UPDATED FILES (mios-bootstrap.git, sibling commit) build-mios.sh - Phase-0 gather_user_choices() prompts for FORGE_ADMIN_USER and FORGE_ADMIN_EMAIL with the same 3-minute timeout (defaulting to LINUX_USER and LINUX_USER@HOSTNAME_VAL.local). - install.env writer emits the three MIOS_FORGE_ADMIN_* lines; password stays empty so firstboot can generate. POSTCHECK COVERAGE No new failures introduced. The new oneshot service ships in /usr/lib/systemd/system/, where systemd-analyze verify already runs in postcheck #10. ConditionVirtualization=!container keeps it from trying to exec into a non-existent Quadlet inside nested containers.
1 parent 7bb042c commit eacc173

4 files changed

Lines changed: 186 additions & 4 deletions

File tree

build-mios.ps1

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,14 @@ if ($DoCustom) {
346346
$LuksPass = if ($UseLuks) { Read-Timed "LUKS passphrase:" "mios" -Secret } else { "" }
347347
$RegistryUrl = Read-Timed "Registry URL:" $DefRegistry
348348

349+
# mios-forge (Forgejo) admin -- defaults to the linux identity above so
350+
# the locally-hosted .git = ./ pattern works without further config.
351+
# Empty password -> firstboot generates a random one and stores it at
352+
# /etc/mios/forge/admin-password (root-owned, mode 0600).
353+
$forgeHostFallback = if ($HostIn) { $HostIn } else { "mios" }
354+
$ForgeAdmin = Read-Timed "Forge admin username (Forgejo):" $U
355+
$ForgeEmail = Read-Timed "Forge admin email:" "$U@$forgeHostFallback.local"
356+
349357
Write-Host ""
350358
Write-Host " Select Deployment Targets (comma separated or 'all'):" -ForegroundColor DarkCyan
351359
Write-Host " 1) RAW, 2) VHDX, 3) WSL, 4) ISO" -ForegroundColor DarkGray
@@ -359,6 +367,8 @@ if ($DoCustom) {
359367
$UseLuks = $false
360368
$LuksPass = ""
361369
$RegistryUrl = $DefRegistry
370+
$ForgeAdmin = $U
371+
$ForgeEmail = if ($env:MIOS_FORGE_ADMIN_EMAIL) { $env:MIOS_FORGE_ADMIN_EMAIL } else { "$U@$(if($HostIn){$HostIn}else{'mios'}).local" }
362372

363373
# Target selection inheritance
364374
if ($env:MIOS_TARGETS) {
@@ -912,7 +922,7 @@ if ($env:MIOS_SKIP_DEPLOY -eq "1") {
912922

913923
# Seed /etc/mios/install.env so wsl-firstboot.service uses the
914924
# operator-supplied identity instead of the default 'mios' password.
915-
if (Write-MiosInstallEnv -WslDistro $WslName -User $U -PasswordHash $passHash -Hostname $HostIn) {
925+
if (Write-MiosInstallEnv -WslDistro $WslName -User $U -PasswordHash $passHash -Hostname $HostIn -ForgeAdminUser $ForgeAdmin -ForgeAdminEmail $ForgeEmail) {
916926
Write-OK "Seeded /etc/mios/install.env (user=$U, host=$HostIn)"
917927
} else {
918928
Write-Warn "install.env not written -- first-boot will fall back to default 'mios' password"
@@ -1023,6 +1033,22 @@ Write-Host " On deployed 'MiOS': mios-rebuild" -ForegroundColor Cyan
10231033
Write-Host " On any machine: podman pull $GhcrImage" -ForegroundColor Cyan
10241034
Write-Host ""
10251035

1036+
# -- mios-forge (Forgejo) post-deploy operator hint --
1037+
# The forge ships disabled-by-default behavior is bounded by the Quadlet's
1038+
# Condition* directives, not by us; but we tell the operator how to reach
1039+
# it once the deployed image boots and mios-forge-firstboot.service has
1040+
# created the admin user from /etc/mios/install.env.
1041+
$forgeUser = if ($ForgeAdmin) { $ForgeAdmin } else { $U }
1042+
$forgeMail = if ($ForgeEmail) { $ForgeEmail } else { "$U@$(if($HostIn){$HostIn}else{'mios'}).local" }
1043+
Write-Host " Self-hosted Git forge (mios-forge / Forgejo)" -ForegroundColor Cyan
1044+
Write-Host " Web UI: http://localhost:3000/" -ForegroundColor Gray
1045+
Write-Host " git+ssh: ssh://git@localhost:2222/<user>/<repo>.git" -ForegroundColor Gray
1046+
Write-Host " Admin user: $forgeUser" -ForegroundColor Gray
1047+
Write-Host " Admin email: $forgeMail" -ForegroundColor Gray
1048+
Write-Host " Initial pwd: sudo cat /etc/mios/forge/admin-password (must change on first login)" -ForegroundColor Gray
1049+
Write-Host " Local push: cd <repo>; git remote add origin http://localhost:3000/$forgeUser/<repo>.git; git push origin main" -ForegroundColor Gray
1050+
Write-Host ""
1051+
10261052
Write-Progress -Activity "'MiOS' Build ${Version}" -Id 0 -Completed
10271053

10281054
Show-StatusCard

tools/lib/install-env.ps1

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ function Write-MiosInstallEnv {
1414
[Parameter(Mandatory)][string]$WslDistro,
1515
[Parameter(Mandatory)][string]$User,
1616
[Parameter(Mandatory)][string]$PasswordHash,
17-
[Parameter(Mandatory)][string]$Hostname
17+
[Parameter(Mandatory)][string]$Hostname,
18+
[string]$ForgeAdminUser = "",
19+
[string]$ForgeAdminEmail = ""
1820
)
1921

2022
# The hash MUST be sha512crypt ($6$salt$digest). Anything else is a bug
@@ -26,17 +28,25 @@ function Write-MiosInstallEnv {
2628
return $false
2729
}
2830

31+
# Default the forge admin to the linux user identity if not supplied.
32+
if (-not $ForgeAdminUser) { $ForgeAdminUser = $User }
33+
if (-not $ForgeAdminEmail) { $ForgeAdminEmail = "$User@$Hostname.local" }
34+
2935
# Single-quote the hash because $6$... contains literal $-sigils that
3036
# bash would otherwise treat as parameter expansion when the env file
3137
# is sourced. sha512crypt charset is [A-Za-z0-9./$] -- no single quotes
3238
# ever appear in the hash, so the wrap is safe.
3339
$lines = @(
3440
"# /etc/mios/install.env -- written by the 'MiOS' Windows installer.",
35-
"# Read by /usr/libexec/mios/wsl-firstboot and /usr/libexec/mios/motd.",
41+
"# Read by /usr/libexec/mios/wsl-firstboot, /usr/libexec/mios/motd,",
42+
"# and /usr/libexec/mios/forge-firstboot.sh.",
3643
"# Vendor defaults: /usr/share/mios/env.defaults",
3744
"MIOS_USER=$User",
3845
"MIOS_USER_PASSWORD_HASH='$PasswordHash'",
39-
"MIOS_HOSTNAME=$Hostname"
46+
"MIOS_HOSTNAME=$Hostname",
47+
"MIOS_FORGE_ADMIN_USER=$ForgeAdminUser",
48+
"MIOS_FORGE_ADMIN_EMAIL=$ForgeAdminEmail",
49+
"MIOS_FORGE_ADMIN_PASSWORD="
4050
)
4151
$body = ($lines -join "`n") + "`n"
4252

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[Unit]
2+
Description='MiOS' Forge first-boot admin-bootstrap (Forgejo)
3+
Documentation=https://github.com/mios-dev/MiOS/blob/main/etc/containers/systemd/mios-forge.container
4+
After=mios-forge.service network-online.target
5+
Wants=mios-forge.service network-online.target
6+
ConditionPathExists=/etc/mios/install.env
7+
ConditionPathExists=!/var/lib/mios/forge/.firstboot-done
8+
ConditionVirtualization=!container
9+
10+
[Service]
11+
Type=oneshot
12+
RemainAfterExit=yes
13+
EnvironmentFile=-/etc/mios/install.env
14+
ExecStart=/usr/libexec/mios/forge-firstboot.sh
15+
TimeoutStartSec=600s
16+
17+
# Hardening: this service writes to a small set of paths, so we lock
18+
# everything else down. The script needs network for the readiness
19+
# probe and podman exec, plus write access to the password / sentinel
20+
# files declared in usr/lib/tmpfiles.d/mios-forge.conf.
21+
NoNewPrivileges=yes
22+
ProtectSystem=strict
23+
ProtectHome=yes
24+
ReadWritePaths=/etc/mios/forge /var/lib/mios/forge /var/log/mios/forge
25+
PrivateTmp=yes
26+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
27+
RestrictNamespaces=yes
28+
LockPersonality=yes
29+
RestrictRealtime=yes
30+
31+
[Install]
32+
WantedBy=multi-user.target
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env bash
2+
# /usr/libexec/mios/forge-firstboot.sh
3+
#
4+
# First-boot admin-bootstrap for the mios-forge Quadlet (Forgejo).
5+
# Runs once via mios-forge-firstboot.service after mios-forge.service is
6+
# healthy. Creates the operator's admin user using values that
7+
# mios-bootstrap install.sh wrote to /etc/mios/install.env, which were
8+
# themselves resolved from mios-bootstrap.git/mios.toml at install time:
9+
#
10+
# MIOS_FORGE_ADMIN_USER defaults to MIOS_LINUX_USER ([identity].username)
11+
# MIOS_FORGE_ADMIN_EMAIL defaults to <user>@<hostname>.local
12+
# MIOS_FORGE_ADMIN_PASSWORD if empty, a 24-byte URL-safe random
13+
# password is generated and written to
14+
# /etc/mios/forge/admin-password (root-owned,
15+
# mode 0600); operator reads it once and
16+
# changes it via the web UI on first login.
17+
#
18+
# Idempotent: marks completion via /var/lib/mios/forge/.firstboot-done
19+
# and short-circuits on every subsequent boot. To re-trigger (e.g. after
20+
# wiping the SQLite DB), delete that sentinel file and restart the
21+
# mios-forge-firstboot.service.
22+
23+
set -euo pipefail
24+
25+
SENTINEL=/var/lib/mios/forge/.firstboot-done
26+
ENV_FILE=/etc/mios/install.env
27+
PASSWORD_FILE=/etc/mios/forge/admin-password
28+
29+
_log() {
30+
logger -t mios-forge-firstboot "$*" 2>/dev/null || true
31+
echo "[forge-firstboot] $*" >&2
32+
}
33+
34+
if [[ -f "$SENTINEL" ]]; then
35+
_log "sentinel present, nothing to do"
36+
exit 0
37+
fi
38+
39+
# Source install.env if present; tolerate absence (operator may run this
40+
# script manually with env-vars set inline).
41+
if [[ -r "$ENV_FILE" ]]; then
42+
# shellcheck source=/dev/null
43+
set -a; source "$ENV_FILE"; set +a
44+
fi
45+
46+
# Resolution: explicit MIOS_FORGE_* wins; otherwise fall back to the
47+
# linux user identity that bootstrap captured. Final fallback to 'mios'.
48+
admin_user="${MIOS_FORGE_ADMIN_USER:-${MIOS_LINUX_USER:-${MIOS_USER:-mios}}}"
49+
admin_host="${MIOS_HOSTNAME:-mios}"
50+
admin_email="${MIOS_FORGE_ADMIN_EMAIL:-${admin_user}@${admin_host}.local}"
51+
admin_password="${MIOS_FORGE_ADMIN_PASSWORD:-}"
52+
53+
# Wait for mios-forge to come up. The Quadlet sets TimeoutStartSec=300s;
54+
# we mirror that ceiling. Forgejo's HTTP listener is the canonical
55+
# readiness probe.
56+
http_port="${MIOS_FORGE_HTTP_PORT:-3000}"
57+
deadline=$(( $(date +%s) + 300 ))
58+
while (( $(date +%s) < deadline )); do
59+
if curl -fsS -o /dev/null "http://localhost:${http_port}/api/v1/version"; then
60+
_log "Forgejo is up on :${http_port}"
61+
break
62+
fi
63+
sleep 2
64+
done
65+
66+
if ! curl -fsS -o /dev/null "http://localhost:${http_port}/api/v1/version"; then
67+
_log "ERROR: Forgejo did not become ready within 300s; aborting"
68+
exit 1
69+
fi
70+
71+
# Generate a password if none was supplied. 24 bytes -> 32 base64 chars.
72+
if [[ -z "$admin_password" ]]; then
73+
admin_password="$(openssl rand -base64 24 | tr -d '\n')"
74+
install -d -m 0750 -o root -g mios-forge /etc/mios/forge
75+
umask 077
76+
printf '%s\n' "$admin_password" > "$PASSWORD_FILE"
77+
chmod 0600 "$PASSWORD_FILE"
78+
chown root:root "$PASSWORD_FILE"
79+
_log "generated initial admin password; wrote $PASSWORD_FILE (mode 0600, root-only)"
80+
fi
81+
82+
# Create the admin via the in-container forgejo CLI. Idempotency: if the
83+
# user already exists (e.g. someone created one via the web UI before
84+
# this service ran), 'forgejo admin user create' returns non-zero and
85+
# we accept that as a soft success.
86+
if podman exec --user mios-forge mios-forge \
87+
forgejo --config /data/gitea/conf/app.ini admin user create \
88+
--admin --must-change-password=true \
89+
--username "$admin_user" \
90+
--email "$admin_email" \
91+
--password "$admin_password" 2>&1 | tee -a /var/log/mios/forge/firstboot.log; then
92+
_log "admin user '${admin_user}' (${admin_email}) created"
93+
else
94+
rc=$?
95+
_log "admin create returned ${rc}; treating as 'already exists' (set MIOS_FORGE_FORCE_FIRSTBOOT=1 to override)"
96+
if [[ "${MIOS_FORGE_FORCE_FIRSTBOOT:-}" == "1" ]]; then
97+
exit "$rc"
98+
fi
99+
fi
100+
101+
install -d -m 0755 -o root -g root "$(dirname "$SENTINEL")"
102+
date -u +%FT%TZ > "$SENTINEL"
103+
chmod 0644 "$SENTINEL"
104+
_log "firstboot complete; sentinel at $SENTINEL"
105+
106+
# One-line operator hint, dropped into the system journal.
107+
_log "Forgejo URL: http://localhost:${http_port}/"
108+
_log "Admin user: ${admin_user}"
109+
_log "Admin email: ${admin_email}"
110+
if [[ -f "$PASSWORD_FILE" ]]; then
111+
_log "Initial password: 'sudo cat ${PASSWORD_FILE}' (must change on first login)"
112+
fi
113+
114+
exit 0

0 commit comments

Comments
 (0)