mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
feat(pods/admin): root admin alias, default-caps cleanup, pod rebuild script
- auth: add `root` as an admin-console route alias (alongside admin/sysop); still gated by $AGENTBBS_ADMINS — the name confers nothing on its own. - pods: drop the now-redundant tuneApt apt-sandbox hack. Rootless podman keeps its default capability set, so apt/chown/su work without disabling the download sandbox. - scripts/rebuild-pods.sh: recreate all member pods (keeps home volumes) so they pick up the current container profile on next `ssh pod@`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
06bc5631d8
commit
a8cb7e3182
5 changed files with 69 additions and 24 deletions
|
|
@ -8,7 +8,8 @@
|
||||||
// emailed code, then offers $99 Founding Lifetime (CoinPay)
|
// emailed code, then offers $99 Founding Lifetime (CoinPay)
|
||||||
// ssh pod@host your personal Linux pod — free for verified members
|
// ssh pod@host your personal Linux pod — free for verified members
|
||||||
// ssh domain@host point your own domain at your homepage (Premium; add/rm/list)
|
// ssh domain@host point your own domain at your homepage (Premium; add/rm/list)
|
||||||
// ssh admin@host the operator admin console ($AGENTBBS_ADMINS only)
|
// ssh admin@host the operator admin console ($AGENTBBS_ADMINS only;
|
||||||
|
// sysop@/root@ are aliases)
|
||||||
// ssh game@host G AgentGames: play game G (e.g. ttt, c4) over NDJSON; rated,
|
// ssh game@host G AgentGames: play game G (e.g. ttt, c4) over NDJSON; rated,
|
||||||
// agent-vs-agent (also on wss://host/play). See docs/agentgames.md
|
// agent-vs-agent (also on wss://host/play). See docs/agentgames.md
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,9 @@ var DomainNames = map[string]bool{"domain": true, "domains": true}
|
||||||
|
|
||||||
// AdminNames are usernames that route to the privileged admin console (PRD §6).
|
// AdminNames are usernames that route to the privileged admin console (PRD §6).
|
||||||
// The route only opens for accounts whose name is in the operator allowlist
|
// The route only opens for accounts whose name is in the operator allowlist
|
||||||
// (see IsAdmin); the name itself confers nothing.
|
// (see IsAdmin); the name itself confers nothing — so "root" is just a familiar
|
||||||
var AdminNames = map[string]bool{"admin": true, "sysop": true}
|
// alias here, not a backdoor.
|
||||||
|
var AdminNames = map[string]bool{"admin": true, "sysop": true, "root": true}
|
||||||
|
|
||||||
// TorURLNames route to the one-shot "fetch a URL over Tor" command (premium).
|
// TorURLNames route to the one-shot "fetch a URL over Tor" command (premium).
|
||||||
var TorURLNames = map[string]bool{"tor-url": true}
|
var TorURLNames = map[string]bool{"tor-url": true}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package auth
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestIsAdminName(t *testing.T) {
|
func TestIsAdminName(t *testing.T) {
|
||||||
for _, name := range []string{"admin", "ADMIN", "sysop"} {
|
for _, name := range []string{"admin", "ADMIN", "sysop", "root"} {
|
||||||
if !IsAdminName(name) {
|
if !IsAdminName(name) {
|
||||||
t.Errorf("IsAdminName(%q) = false, want true", name)
|
t.Errorf("IsAdminName(%q) = false, want true", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ func (m *Manager) ensure(user string) (string, error) {
|
||||||
// Already exists?
|
// Already exists?
|
||||||
if err := exec.Command(m.engine, "container", "inspect", name).Run(); err == nil {
|
if err := exec.Command(m.engine, "container", "inspect", name).Run(); err == nil {
|
||||||
_ = exec.Command(m.engine, "start", name).Run() // no-op if running
|
_ = exec.Command(m.engine, "start", name).Run() // no-op if running
|
||||||
m.tuneApt(name)
|
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
args := []string{
|
args := []string{
|
||||||
|
|
@ -149,28 +148,9 @@ func (m *Manager) ensure(user string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("pods: create failed: %v: %s", err, strings.TrimSpace(string(out)))
|
return "", fmt.Errorf("pods: create failed: %v: %s", err, strings.TrimSpace(string(out)))
|
||||||
}
|
}
|
||||||
m.tuneApt(name)
|
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tuneApt makes apt usable inside the hardened pod. apt drops privileges to
|
|
||||||
// the _apt user for downloads (setgroups/setegid/seteuid), which needs
|
|
||||||
// CAP_SETUID/CAP_SETGID/CAP_CHOWN — caps we intentionally drop (cap-drop ALL).
|
|
||||||
// Rather than re-grant those to the whole container, disable apt's download
|
|
||||||
// sandbox so package management runs as the pod's (rootless-mapped) root.
|
|
||||||
//
|
|
||||||
// Only applies to the podman/container-root path; under docker the pod runs as
|
|
||||||
// uid 1000 and can't write /etc/apt (apt isn't usable there by design). Failure
|
|
||||||
// is non-fatal: a missing config just means the user sees the old apt errors.
|
|
||||||
func (m *Manager) tuneApt(name string) {
|
|
||||||
if m.engine == "docker" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = exec.Command(m.engine, "exec", "--user", "root", name,
|
|
||||||
"sh", "-c", `printf 'APT::Sandbox::User "root";\n' > /etc/apt/apt.conf.d/00no-sandbox`,
|
|
||||||
).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach provisions the pod and wires the SSH session to a shell inside it.
|
// Attach provisions the pod and wires the SSH session to a shell inside it.
|
||||||
// Blocks until the shell exits or the session closes.
|
// Blocks until the shell exits or the session closes.
|
||||||
func (m *Manager) Attach(s ssh.Session, user string) error {
|
func (m *Manager) Attach(s ssh.Session, user string) error {
|
||||||
|
|
|
||||||
63
scripts/rebuild-pods.sh
Executable file
63
scripts/rebuild-pods.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rebuild every AgentBBS member pod so it picks up the current container profile
|
||||||
|
# (e.g. the rootless-podman default capability set added for apt/chown/su/:80).
|
||||||
|
#
|
||||||
|
# It removes each pod CONTAINER but keeps that pod's named home volume
|
||||||
|
# (agentbbs-pod-<name>-home) and the host-side public_html, so member data and
|
||||||
|
# websites are untouched. Pods are recreated automatically — with the new
|
||||||
|
# profile — the next time each member runs `ssh pod@<host>`. Caddy serves
|
||||||
|
# public_html from the host, so sites stay up while a pod is briefly down.
|
||||||
|
#
|
||||||
|
# Anything a member installed into the pod's system rootfs (apt packages, etc.)
|
||||||
|
# is lost on rebuild; only /home/dev and public_html persist.
|
||||||
|
#
|
||||||
|
# Run this as the user that owns the pods. For rootless podman that's the
|
||||||
|
# AgentBBS service user (pods are per-user), not necessarily root.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/rebuild-pods.sh # list, then prompt before removing
|
||||||
|
# scripts/rebuild-pods.sh --yes # non-interactive (for cron/deploy)
|
||||||
|
# AGENTBBS_POD_ENGINE=docker scripts/rebuild-pods.sh # force engine
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ENGINE="${AGENTBBS_POD_ENGINE:-}"
|
||||||
|
if [ -z "$ENGINE" ]; then
|
||||||
|
if command -v podman >/dev/null 2>&1; then
|
||||||
|
ENGINE=podman
|
||||||
|
elif command -v docker >/dev/null 2>&1; then
|
||||||
|
ENGINE=docker
|
||||||
|
else
|
||||||
|
echo "rebuild-pods: neither podman nor docker found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t pods < <("$ENGINE" ps -a --filter 'name=agentbbs-pod-' --format '{{.Names}}' | sort)
|
||||||
|
|
||||||
|
if [ "${#pods[@]}" -eq 0 ]; then
|
||||||
|
echo "rebuild-pods: no pods found (engine: $ENGINE)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found ${#pods[@]} pod(s) via $ENGINE:"
|
||||||
|
printf ' %s\n' "${pods[@]}"
|
||||||
|
|
||||||
|
if [ "${1:-}" != "--yes" ] && [ "${1:-}" != "-y" ]; then
|
||||||
|
printf 'Remove these containers (home volumes kept)? [y/N] '
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
y | Y | yes | YES) ;;
|
||||||
|
*)
|
||||||
|
echo "aborted"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
for p in "${pods[@]}"; do
|
||||||
|
# No -v: named home volumes are preserved, only the container is destroyed.
|
||||||
|
"$ENGINE" rm -f "$p" >/dev/null && echo "removed $p"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done. Each pod recreates with the new profile on its owner's next 'ssh pod@'."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue