Add LDAP byte-pump tunnel, secrets rendering, and IAM engine
See CHANGELOG.md for the full breakdown. Summary:
- ldap_tunnel.go: serves a local unix socket for SSSD/PAM and relays raw
bytes to the SSO over the existing WSS channel (ldap_tunnel messages);
the agent never parses LDAP (DESIGN.md §4). Adds safeWriter to
serialize WebSocket writes now that telemetry, heartbeat, the LDAP
tunnel, and command responses all share one connection.
- secrets.go: renders local templates ({{ bao "path#key" }} placeholders)
by fetching node-scoped values from the SSO and writing the target
atomically at 0600, on a signed render_secrets command (DESIGN.md §5).
demo/ has minimal bash + Node consumers of the rendered file.
- iam.go: applies signed node IAM pushes -- sudoers.d rules (visudo -c
validated), SSH AuthorizedKeysCommand keys, /etc/security/access.conf,
and revocation via sss_cache -E + pkill -u (DESIGN.md §6).
- Capability reporting: the agent's enabled capabilities ride along in
its discovery frame so the SSO can show them in the Directory.
- DESIGN.md: the v2 protocol design this implements.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ldapTunnel is the agent's local LDAP socket (DESIGN.md §4). It is a pure byte
|
||||
// pump: bytes from a local LDAP client (SSSD) are forwarded to the SSO over the
|
||||
// WSS channel as `ldap_tunnel` messages, and the SSO's responses are written
|
||||
// back to the socket. The agent never parses LDAP — it does not know or care
|
||||
// what the bytes mean.
|
||||
//
|
||||
// Each local connection gets a conn_id. The agent reads the socket and sends
|
||||
// chunks up; the SSO relays them into its real OpenLDAP and sends the response
|
||||
// chunks back down; the agent writes them to the socket. `close:true` ends a
|
||||
// connection.
|
||||
type ldapTunnel struct {
|
||||
mu sync.Mutex
|
||||
conns map[string]net.Conn
|
||||
send func(WSMessage) error
|
||||
}
|
||||
|
||||
func newLdapTunnel(send func(WSMessage) error) *ldapTunnel {
|
||||
return &ldapTunnel{
|
||||
conns: make(map[string]net.Conn),
|
||||
send: send,
|
||||
}
|
||||
}
|
||||
|
||||
// start binds the unix socket and accepts connections until stopCh closes.
|
||||
func (t *ldapTunnel) start(socketPath string, stopCh <-chan struct{}) {
|
||||
// Remove a stale socket left over from a previous run, and make sure the
|
||||
// parent directory exists (e.g. /run/theta on a fresh boot).
|
||||
os.Remove(socketPath)
|
||||
if dir := filepath.Dir(socketPath); dir != "." && dir != "/" {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
log.Printf("LDAP tunnel: cannot bind %s: %v", socketPath, err)
|
||||
return
|
||||
}
|
||||
// root:theta, 0660 — only root and the theta group can connect. A unix
|
||||
// socket is preferred over 127.0.0.1:389 because filesystem permissions
|
||||
// restrict which local processes can reach it.
|
||||
os.Chmod(socketPath, 0660)
|
||||
defer ln.Close()
|
||||
log.Printf("LDAP tunnel: listening on %s", socketPath)
|
||||
|
||||
go func() {
|
||||
<-stopCh
|
||||
ln.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go t.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConn pumps one local LDAP connection up to the SSO.
|
||||
func (t *ldapTunnel) handleConn(conn net.Conn) {
|
||||
connID := newConnID()
|
||||
t.mu.Lock()
|
||||
t.conns[connID] = conn
|
||||
t.mu.Unlock()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
msg := WSMessage{
|
||||
Type: "ldap_tunnel",
|
||||
Payload: map[string]interface{}{
|
||||
"conn_id": connID,
|
||||
"data": base64.StdEncoding.EncodeToString(buf[:n]),
|
||||
},
|
||||
}
|
||||
if err := t.send(msg); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Signal end of connection to the SSO so it can close its OpenLDAP relay.
|
||||
t.send(WSMessage{
|
||||
Type: "ldap_tunnel",
|
||||
Payload: map[string]interface{}{
|
||||
"conn_id": connID,
|
||||
"close": true,
|
||||
},
|
||||
})
|
||||
|
||||
t.mu.Lock()
|
||||
delete(t.conns, connID)
|
||||
t.mu.Unlock()
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
// handleMessage writes SSO→agent tunnel bytes to the matching local socket.
|
||||
// Called from handleCommand when an `ldap_tunnel` message arrives.
|
||||
func (t *ldapTunnel) handleMessage(payload map[string]interface{}) {
|
||||
connID, _ := payload["conn_id"].(string)
|
||||
if connID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if closeFlag, _ := payload["close"].(bool); closeFlag {
|
||||
t.mu.Lock()
|
||||
conn := t.conns[connID]
|
||||
delete(t.conns, connID)
|
||||
t.mu.Unlock()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dataStr, _ := payload["data"].(string)
|
||||
if dataStr == "" {
|
||||
return
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(dataStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
conn := t.conns[connID]
|
||||
t.mu.Unlock()
|
||||
if conn != nil {
|
||||
conn.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
var connCounter uint64
|
||||
|
||||
func newConnID() string {
|
||||
connCounter++
|
||||
return fmt.Sprintf("%d-%d", time.Now().UnixNano(), connCounter)
|
||||
}
|
||||
|
||||
// safeWriter serializes writes to the WebSocket. Gorilla allows only one
|
||||
// concurrent writer, but the agent has several (telemetry, heartbeat, the LDAP
|
||||
// tunnel, command responses) — without this, concurrent WriteMessage calls
|
||||
// corrupt the stream.
|
||||
type safeWriter struct {
|
||||
mu sync.Mutex
|
||||
c *websocket.Conn
|
||||
}
|
||||
|
||||
func (w *safeWriter) WriteMessage(messageType int, data []byte) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.c.WriteMessage(messageType, data)
|
||||
}
|
||||
|
||||
// sendTunnelMessage marshals a WSMessage and writes it as a text frame.
|
||||
func sendTunnelMessage(w MessageWriter, msg WSMessage) error {
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteMessage(websocket.TextMessage, payload)
|
||||
}
|
||||
Reference in New Issue
Block a user