v0.21.0 scram password auth, cred and token management, browser tls-terminating proxy, viewer client
Test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-02 15:25:51 -04:00
1 parent f192c26fa2
commit 9ab8c356d2
60 files changed
+6791 -747

No files matched your search

+33 -6
View File
@@ -95,6 +95,8 @@ func NewHTTPChainSourcePlugin(
opts.IngestPath = DefaultHTTPChainSourceIngestPath
} else if !strings.HasPrefix(opts.IngestPath, "/") {
return nil, fmt.Errorf("ingest_path: must start with '/'")
} else if opts.IngestPath == chain.AuthPath {
return nil, fmt.Errorf("ingest_path: %s is reserved for authentication", chain.AuthPath)
}
if opts.BufferSize <= 0 {
opts.BufferSize = DefaultHTTPChainSourceBufferSize
@@ -109,7 +111,7 @@ func NewHTTPChainSourcePlugin(
if err != nil {
return nil, err
}
authPolicy, err := authz.New(opts.Auth, opts.TLS, authz.RoleChainListener)
authPolicy, err := authz.New(opts.Auth, tlsCfg, authz.RoleChainListener, authz.HTTP)
if err != nil {
return nil, err
}
@@ -163,16 +165,22 @@ func (s *HTTPChainSource) Subscribe() <-chan core.LogEntry {
// Start binds the listener and serves the ingest endpoint
func (s *HTTPChainSource) Start() error {
if err := s.auth.Start(); err != nil {
return err
}
addr := net.JoinHostPort(s.config.Host, strconv.FormatInt(s.config.Port, 10))
// IPv4-only, aligns with tcp/http sinks
ln, err := net.Listen("tcp4", addr)
if err != nil {
s.auth.Close()
return fmt.Errorf("listen %s: %w", addr, err)
}
mux := http.NewServeMux()
// Method-scoped pattern: mux answers 405 with Allow header on non-POST
mux.HandleFunc(http.MethodPost+" "+s.config.IngestPath, s.handleIngest)
// Answers 404 unless the policy is scram
mux.HandleFunc(http.MethodPost+" "+chain.AuthPath, s.handleAuth)
s.server = &http.Server{
Handler: mux,
@@ -212,6 +220,7 @@ func (s *HTTPChainSource) Stop() {
defer cancel()
s.server.Shutdown(ctx)
}
s.auth.Close()
s.sessionsMu.Lock()
for _, id := range s.sessions {
@@ -269,10 +278,9 @@ func (s *HTTPChainSource) handleIngest(w http.ResponseWriter, r *http.Request) {
s.totalRequests.Add(1)
// Authorize before the body is read: an unauthorized sender should not get
// to stream max_body_bytes into the process. 403 is distinct from the 400
// used for protocol errors, so a sender can tell "not allowed" from
// "malformed batch".
ident, err := s.auth.Authorize(r.TLS)
// to stream max_body_bytes into the process. 401 (log in again) and 403
// (not allowed) are distinct from the 400 used for protocol errors.
ident, status, err := s.auth.AuthorizeRequest(r)
if err != nil {
s.rejectedRequests.Add(1)
s.logger.Warn("msg", "Request rejected by auth policy",
@@ -280,7 +288,7 @@ func (s *HTTPChainSource) handleIngest(w http.ResponseWriter, r *http.Request) {
"instance_id", s.id,
"remote_addr", r.RemoteAddr,
"error", err)
http.Error(w, "forbidden", http.StatusForbidden)
authz.Refuse(w, status)
return
}
@@ -354,6 +362,25 @@ func (s *HTTPChainSource) handleIngest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// handleAuth runs one step of a SCRAM login and logs its outcome
func (s *HTTPChainSource) handleAuth(w http.ResponseWriter, r *http.Request) {
ident, err := s.auth.ServeAuth(w, r)
switch {
case err != nil:
s.logger.Warn("msg", "Login rejected",
"component", "http_chain_source",
"instance_id", s.id,
"remote_addr", r.RemoteAddr,
"error", err)
case ident.Name != "":
s.logger.Info("msg", "Login accepted",
"component", "http_chain_source",
"instance_id", s.id,
"remote_addr", r.RemoteAddr,
"auth_identity", ident.Name)
}
}
// sessionFor returns the cached session for a remote+node+identity,
// recreating after idle expiry. Identity is part of the key so two peers
// sharing a remote address never share a session.
+34 -34
View File
@@ -96,7 +96,7 @@ func NewTCPChainSourcePlugin(
if err != nil {
return nil, err
}
authPolicy, err := authz.New(opts.Auth, opts.TLS, authz.RoleChainListener)
authPolicy, err := authz.New(opts.Auth, tlsCfg, authz.RoleChainListener, authz.TCP)
if err != nil {
return nil, err
}
@@ -149,11 +149,15 @@ func (s *TCPChainSource) Subscribe() <-chan core.LogEntry {
// Start binds the listener and begins accepting connections
func (s *TCPChainSource) Start() error {
if err := s.auth.Start(); err != nil {
return err
}
addr := net.JoinHostPort(s.config.Host, strconv.FormatInt(s.config.Port, 10))
// IPv4-only. TLS-wrapped when configured; handshake runs explicitly in
// handleConn under tlsx.HandshakeTimeout, pre-hello.
ln, err := net.Listen("tcp4", addr)
if err != nil {
s.auth.Close()
return fmt.Errorf("listen %s: %w", addr, err)
}
if s.tlsConfig != nil {
@@ -189,6 +193,7 @@ func (s *TCPChainSource) Stop() {
s.mu.Unlock()
s.wg.Wait()
s.auth.Close()
s.mu.Lock()
for _, ch := range s.subscribers {
@@ -292,56 +297,51 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
tlsState = &cs
}
// Authorize before a preamble is parsed on an unauthorized peer's behalf
ident, err := s.auth.Authorize(tlsState)
// Certificates are checked before the hello is read; under scram the hello
// carries the exchange. Nothing is acknowledged until node binding agrees.
adm, err := s.auth.Admit(conn, tlsState, true, time.Duration(s.config.HelloTimeoutMS)*time.Millisecond)
if err != nil {
s.rejectedConns.Add(1)
s.logger.Warn("msg", "Connection rejected by auth policy",
"component", "tcp_chain_source",
"instance_id", s.id,
"remote_addr", remote,
"error", err)
return // deferred cleanup closes conn
}
scanner := bufio.NewScanner(conn)
// Oversized line (> MaxLogEntryBytes) is a protocol violation; scanner is
// unrecoverable after ErrTooLong, connection terminates
scanner.Buffer(make([]byte, 0, 64*1024), core.MaxLogEntryBytes)
// Hello preamble
conn.SetReadDeadline(time.Now().Add(time.Duration(s.config.HelloTimeoutMS) * time.Millisecond))
if !scanner.Scan() {
s.logger.Warn("msg", "Connection closed before hello",
"component", "tcp_chain_source",
"remote_addr", remote,
"error", scanner.Err())
return
}
hello, err := chain.DecodeHello(scanner.Bytes())
if err != nil {
s.logger.Warn("msg", "Rejected chain connection",
"component", "tcp_chain_source",
"remote_addr", remote,
"error", err)
if errors.Is(err, authz.ErrRefused) {
s.rejectedConns.Add(1)
s.logger.Warn("msg", "Connection rejected by auth policy",
"component", "tcp_chain_source",
"instance_id", s.id,
"remote_addr", remote,
"error", err)
} else {
s.logger.Warn("msg", "Rejected chain connection",
"component", "tcp_chain_source",
"remote_addr", remote,
"error", err)
}
return
}
ident := adm.Identity
fallbackNode := remote
if host, _, splitErr := net.SplitHostPort(remote); splitErr == nil {
fallbackNode = host
}
connNode, err := s.auth.ResolveNode(hello.Node, fallbackNode, s.config.TrustNode, ident)
connNode, err := s.auth.ResolveNode(adm.Hello.Node, fallbackNode, s.config.TrustNode, ident)
if err != nil {
s.rejectedConns.Add(1)
adm.Reject("node label rejected")
s.logger.Warn("msg", "Connection rejected by node binding",
"component", "tcp_chain_source",
"instance_id", s.id,
"remote_addr", remote,
"declared_node", hello.Node,
"declared_node", adm.Hello.Node,
"error", err)
return
}
if err := adm.Accept(); err != nil {
return
}
scanner := bufio.NewScanner(adm.Reader)
// Oversized line (> MaxLogEntryBytes) is a protocol violation; scanner is
// unrecoverable after ErrTooLong, connection terminates
scanner.Buffer(make([]byte, 0, 64*1024), core.MaxLogEntryBytes)
// force relabels every entry, so an edge cannot smuggle a foreign origin
// through the per-entry node field either
trustEntryNode := s.auth.TrustsEntryNode(s.config.TrustNode)