mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
feat(pods): SSH agent forwarding → git push from the pod with your key
Code in your pod and push to git.profullstack.com using the SAME SSH key you signed in with — nothing is copied into the pod. When a member attaches with agent forwarding (ssh -A), agentbbs listens on a fresh unix socket in a per-user agent dir bind-mounted at /run/agentbbs-agent and proxies it back over the session; the pod shell gets SSH_AUTH_SOCK pointed at it. The pod image's ssh_config sends git@git.profullstack.com to Forgejo's SSH server (:2222), so `git clone git@git.profullstack.com:you/repo.git` just works. - pods.go: agentDir + startAgent (per-session socket, cleaned up on exit); Attach injects SSH_AUTH_SOCK when ssh.AgentRequested; ensure() bind-mounts the agent dir and self-heals idle pods missing it. No main.go change needed — charmbracelet/ssh sets AgentRequested from the session request loop. - pods/Containerfile: /etc/ssh/ssh_config.d entry (port 2222, user git, accept-new) so the conventional git@ URL reaches Forgejo. - setup.sh: keep using an already-built pod image if a later rebuild transient-fails, so a flaky deploy never downgrades pods to the base image. Build/vet/test/gofmt clean. Image rebuilt on the host; `ssh -G git.profullstack.com` resolves to port 2222 / user git. End-to-end push needs a live `ssh -A` session (validate after deploy). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
281d4b55fd
commit
94ce79374c
3 changed files with 81 additions and 5 deletions
|
|
@ -13,8 +13,11 @@
|
|||
package pods
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -111,6 +114,45 @@ func (m *Manager) hasImage(name, image string) bool {
|
|||
return norm(got) == norm(image)
|
||||
}
|
||||
|
||||
// agentDir is the host directory bind-mounted into a pod at /run/agentbbs-agent,
|
||||
// where Attach drops a forwarded SSH-agent socket. Derived as <data>/agent/<user>
|
||||
// (a sibling of the users dir). Empty — disabling agent forwarding — when the
|
||||
// users dir isn't configured or the directory can't be created.
|
||||
func (m *Manager) agentDir(user string) string {
|
||||
if m.usersDir == "" {
|
||||
return ""
|
||||
}
|
||||
d := filepath.Join(filepath.Dir(m.usersDir), "agent", unsafeName.ReplaceAllString(strings.ToLower(user), "-"))
|
||||
if err := os.MkdirAll(d, 0o700); err != nil {
|
||||
return ""
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// startAgent forwards the connecting client's SSH agent into the member's pod:
|
||||
// it listens on a fresh unix socket in the bind-mounted agent dir and proxies
|
||||
// connections back over the SSH session. Returns the in-pod SSH_AUTH_SOCK path
|
||||
// and a cleanup func, or "" when forwarding can't be set up (no agent dir / no
|
||||
// socket). With this, `git push git@git.profullstack.com` inside the pod uses
|
||||
// the member's own key — nothing is copied into the pod.
|
||||
func (m *Manager) startAgent(s ssh.Session, user string) (sock string, cleanup func()) {
|
||||
dir := m.agentDir(user)
|
||||
if dir == "" {
|
||||
return "", func() {}
|
||||
}
|
||||
var b [8]byte
|
||||
_, _ = rand.Read(b[:])
|
||||
fname := "agent-" + hex.EncodeToString(b[:]) + ".sock"
|
||||
hostSock := filepath.Join(dir, fname)
|
||||
_ = os.Remove(hostSock)
|
||||
l, err := net.Listen("unix", hostSock)
|
||||
if err != nil {
|
||||
return "", func() {}
|
||||
}
|
||||
go ssh.ForwardAgentConnections(l, s)
|
||||
return "/run/agentbbs-agent/" + fname, func() { _ = l.Close(); _ = os.Remove(hostSock) }
|
||||
}
|
||||
|
||||
// Engine reports the active container engine.
|
||||
func (m *Manager) Engine() string { return m.engine }
|
||||
|
||||
|
|
@ -126,6 +168,9 @@ func (m *Manager) ensure(user string) (string, error) {
|
|||
// Bind the host's public_html into the pod so a member's edits at
|
||||
// ~/public_html are exactly what Caddy serves at <name>.<host>.
|
||||
_, pubSpec := m.publicHTMLMount(user)
|
||||
// Bind a per-user agent dir into the pod; Attach drops a forwarded SSH-agent
|
||||
// socket here so `git push` uses the member's own key (see startAgent).
|
||||
agentDir := m.agentDir(user)
|
||||
if m.engine == "docker" {
|
||||
// Under docker the pod runs as uid 1000 (never container root), so the
|
||||
// named home volume — and the bind-mounted public_html — must be owned
|
||||
|
|
@ -159,7 +204,9 @@ func (m *Manager) ensure(user string) (string, error) {
|
|||
// running an out-of-date image (e.g. a new pod image with added tooling).
|
||||
// The home volume persists across rm, so member data is kept; a busy pod
|
||||
// heals on its next idle attach instead.
|
||||
needsHeal := idle && ((pubSpec != "" && !m.hasMount(name, "/home/dev/public_html")) || !m.hasImage(name, m.image))
|
||||
needsHeal := idle && ((pubSpec != "" && !m.hasMount(name, "/home/dev/public_html")) ||
|
||||
(agentDir != "" && !m.hasMount(name, "/run/agentbbs-agent")) ||
|
||||
!m.hasImage(name, m.image))
|
||||
if needsHeal {
|
||||
_ = exec.Command(m.engine, "rm", "-f", name).Run() // fall through to recreate
|
||||
} else {
|
||||
|
|
@ -182,6 +229,9 @@ func (m *Manager) ensure(user string) (string, error) {
|
|||
if pubSpec != "" {
|
||||
args = append(args, "-v", pubSpec)
|
||||
}
|
||||
if agentDir != "" {
|
||||
args = append(args, "-v", agentDir+":/run/agentbbs-agent")
|
||||
}
|
||||
if m.engine == "docker" {
|
||||
// Rootful docker: a breakout is host-root, so refuse to hand out
|
||||
// container root — run as uid 1000 with no caps and no privilege
|
||||
|
|
@ -221,14 +271,22 @@ func (m *Manager) Attach(s ssh.Session, user string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Forward the client's SSH agent (ssh -A) into the pod so git push uses the
|
||||
// member's own key. No-op unless the client requested forwarding.
|
||||
execEnv := []string{"-e", "TERM=" + ptyReq.Term}
|
||||
if ssh.AgentRequested(s) {
|
||||
if sock, cleanup := m.startAgent(s, user); sock != "" {
|
||||
defer cleanup()
|
||||
execEnv = append(execEnv, "-e", "SSH_AUTH_SOCK="+sock)
|
||||
}
|
||||
}
|
||||
|
||||
shell := env("AGENTBBS_POD_SHELL", "/bin/bash")
|
||||
cmd := exec.Command(m.engine, "exec", "-it",
|
||||
"-e", "TERM="+ptyReq.Term,
|
||||
name, shell, "-l")
|
||||
cmd := exec.Command(m.engine, append(append([]string{"exec", "-it"}, execEnv...), name, shell, "-l")...)
|
||||
f, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
// busybox-ish images may lack bash
|
||||
cmd = exec.Command(m.engine, "exec", "-it", "-e", "TERM="+ptyReq.Term, name, "/bin/sh", "-l")
|
||||
cmd = exec.Command(m.engine, append(append([]string{"exec", "-it"}, execEnv...), name, "/bin/sh", "-l")...)
|
||||
f, err = pty.Start(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pods: attach failed: %w", err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue