fix: remove terminal module and implement concurrent log stream limiting
build-and-release / release (push) Successful in 2m39s
build-and-release / release (push) Successful in 2m39s
This commit is contained in:
+67
-20
@@ -31,7 +31,6 @@ import (
|
||||
"nadir/internal/modules/services"
|
||||
"nadir/internal/modules/storage"
|
||||
"nadir/internal/modules/system"
|
||||
"nadir/internal/modules/terminal"
|
||||
"nadir/internal/modules/users"
|
||||
"nadir/internal/rbac"
|
||||
|
||||
@@ -206,13 +205,12 @@ func runServer() {
|
||||
mods := []module.Module{
|
||||
system.New(),
|
||||
services.New(cfg.LogFiles),
|
||||
users.New(),
|
||||
users.New(sessions),
|
||||
groups.New(),
|
||||
packages.New(),
|
||||
networking.New(),
|
||||
storage.New(),
|
||||
audit.New(auditStore),
|
||||
terminal.New(sessions),
|
||||
}
|
||||
|
||||
roles := rbac.New()
|
||||
@@ -236,6 +234,8 @@ func runServer() {
|
||||
humaConfig.DocsPath = ""
|
||||
|
||||
api := humago.New(mux, humaConfig)
|
||||
rateLimiter := auth.NewRateLimiter(100, time.Minute)
|
||||
api.UseMiddleware(auth.RateLimitMiddleware(api, rateLimiter))
|
||||
api.UseMiddleware(rbac.RbacMiddleware(api, sessions, tokenAuth, roles, auditStore))
|
||||
|
||||
for _, m := range mods {
|
||||
@@ -277,18 +277,18 @@ func runServer() {
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /docs", func(w http.ResponseWriter, _ *http.Request) {
|
||||
// /docs needs to execute the Scalar bundle, so loosen the strict CSP set
|
||||
// by secHeaders for this one page: allow scripts/styles from the jsdelivr
|
||||
// CDN plus inline (Scalar uses inline <script> + inline styles). The CDN
|
||||
// host is the supply-chain trust boundary; pinning a version + SRI here
|
||||
// is the follow-up (M5 partial).
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; "+
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "+
|
||||
"img-src 'self' data: https:; "+
|
||||
"connect-src 'self'; "+
|
||||
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com")
|
||||
// /docs needs to execute the Scalar bundle, so loosen the strict CSP set
|
||||
// by secHeaders for this one page: allow scripts/styles from the pinned
|
||||
// jsdelivr CDN version + inline (Scalar uses inline <script> + inline styles).
|
||||
// unsafe-eval is removed Scalar does not need it. The CDN host is the
|
||||
// supply-chain trust boundary; SRI pinning would close the remaining gap.
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "+
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "+
|
||||
"img-src 'self' data: https:; "+
|
||||
"connect-src 'self'; "+
|
||||
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!doctype html><html><head><title>API</title>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -303,7 +303,7 @@ func runServer() {
|
||||
Addr: addr,
|
||||
// WithClientIP records the source IP for the login throttle (H1); behind a
|
||||
// trusted proxy it reads X-Forwarded-For instead of the proxy's address.
|
||||
Handler: secHeaders(auth.WithClientIP(cfg.Server.TrustProxy, mux)),
|
||||
Handler: bodySizeLimit(requestTimeout(secHeaders(auth.WithClientIP(cfg.Server.TrustProxy, mux)))),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 0, // unset: SSE endpoints stream indefinitely
|
||||
@@ -313,14 +313,14 @@ func runServer() {
|
||||
// Pick how the connection is secured (see config.Server doc):
|
||||
// 1. trust_proxy - a reverse proxy terminates TLS; we serve plaintext HTTP.
|
||||
// 2. tls_cert/tls_key - we terminate TLS with the admin's PEM pair.
|
||||
// 3. neither - a fresh in-memory self-signed cert (dev only).
|
||||
// 3. neither - plain HTTP (localhost-only default).
|
||||
var serve func() error
|
||||
switch {
|
||||
case cfg.Server.TrustProxy:
|
||||
log.Printf("tls: trust_proxy set - serving plaintext HTTP, TLS terminated upstream; bind to localhost so X-Forwarded-For can't be spoofed")
|
||||
log.Printf("tls: trust_proxy set — serving plaintext HTTP, TLS terminated upstream; bind to localhost so X-Forwarded-For can't be spoofed")
|
||||
serve = srv.ListenAndServe
|
||||
default:
|
||||
cert, err := serverCert(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
||||
case cfg.Server.TLSCert != "" && cfg.Server.TLSKey != "":
|
||||
cert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
||||
if err != nil {
|
||||
log.Fatalf("tls cert: %v", err)
|
||||
}
|
||||
@@ -331,8 +331,19 @@ func runServer() {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
}
|
||||
serve = func() error { return srv.ListenAndServeTLS("", "") }
|
||||
default:
|
||||
log.Printf("tls: no TLS configured — serving plain HTTP on %s", addr)
|
||||
serve = srv.ListenAndServe
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -360,6 +371,40 @@ func runServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// bodySizeLimit rejects requests with a body larger than 1 MB, preventing
|
||||
// OOM from arbitrarily large JSON payloads. Wraps the entire mux so it
|
||||
// covers both Huma-registered routes and raw mux handlers (/docs, /install.sh).
|
||||
func bodySizeLimit(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// requestTimeout cancels slow requests via context deadline. SSE endpoints
|
||||
// (package streams, log following) are exempt because they keep the
|
||||
// connection open indefinitely.
|
||||
func requestTimeout(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if isSSEEndpoint(r) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// isSSEEndpoint identifies routes that stream data indefinitely and must
|
||||
// not have a context deadline.
|
||||
func isSSEEndpoint(r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/packages") {
|
||||
return r.Method == "POST" || r.Method == "DELETE"
|
||||
}
|
||||
return r.Method == "GET" && strings.HasSuffix(r.URL.Path, "/logs/stream")
|
||||
}
|
||||
|
||||
// secHeaders sets defensive response headers on every HTTP response. The
|
||||
// default Content-Security-Policy denies everything (`default-src 'none'`) —
|
||||
// correct for the JSON API and the tiny landing/favicon endpoints. /docs
|
||||
@@ -377,6 +422,8 @@ func secHeaders(next http.Handler) http.Handler {
|
||||
h.Set("Referrer-Policy", "no-referrer")
|
||||
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
|
||||
h.Set("Pragma", "no-cache")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,9 +132,9 @@ func installService(args []string) error {
|
||||
return fmt.Errorf("options --tls, --unsecure, and --trust-proxy are mutually exclusive")
|
||||
}
|
||||
|
||||
// Default to tls if nothing is specified
|
||||
isTLS := *tlsOpt || optCount == 0
|
||||
isUnsecure := *unsecureOpt
|
||||
// Default to unsecure (plain HTTP) if nothing is specified
|
||||
isTLS := *tlsOpt
|
||||
isUnsecure := *unsecureOpt || optCount == 0
|
||||
isTrustProxy := *trustProxyOpt
|
||||
|
||||
// Provision the PAM service the server authenticates against, so it exists
|
||||
@@ -505,7 +505,7 @@ func fatalIf(err error) {
|
||||
|
||||
// DefaultConfigContent returns the default configuration template filled with the username.
|
||||
func DefaultConfigContent(username string) string {
|
||||
return fmt.Sprintf(configTemplateBase, "true", "# trust_proxy: false", "# tls_cert: /var/lib/nadir/tls/cert.pem", "# tls_key: /var/lib/nadir/tls/key.pem", "127.0.0.1", 9999, username)
|
||||
return fmt.Sprintf(configTemplateBase, "false", "# trust_proxy: false", "# tls_cert: /var/lib/nadir/tls/cert.pem", "# tls_key: /var/lib/nadir/tls/key.pem", "127.0.0.1", 9999, username)
|
||||
}
|
||||
|
||||
// saveDefaultConfig writes the default configuration template to cfgPath.
|
||||
@@ -531,8 +531,8 @@ Usage:
|
||||
nadir [run] [-d] [-f <path>] Start the server (-d / --detach: run in background)
|
||||
nadir --save-config [-f <path>] Save default configuration to path and exit
|
||||
nadir install [--tls|--unsecure|--trust-proxy] Install + enable the systemd service (starts on boot)
|
||||
(--tls: enable HTTPS with self-signed certificate, default)
|
||||
(--unsecure: serve HTTP directly)
|
||||
(--tls: enable HTTPS with self-signed certificate)
|
||||
(--unsecure: serve HTTP directly, default)
|
||||
(--trust-proxy: serve HTTP behind a reverse proxy)
|
||||
nadir uninstall [--complete] Remove the service (keeps data/config; --complete wipes all)
|
||||
nadir start|stop|restart|status Control the running service
|
||||
|
||||
+1
-46
@@ -4,60 +4,15 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// serverCert returns the TLS certificate to serve: the admin-supplied PEM pair
|
||||
// when both paths are set, otherwise a freshly generated in-memory self-signed
|
||||
// cert for local development.
|
||||
func serverCert(certPath, keyPath string) (tls.Certificate, error) {
|
||||
if certPath != "" && keyPath != "" {
|
||||
return tls.LoadX509KeyPair(certPath, keyPath)
|
||||
}
|
||||
log.Printf("tls: no tls_cert/tls_key configured - generating a self-signed certificate (dev only)")
|
||||
return generateSelfSignedCert()
|
||||
}
|
||||
|
||||
func generateSelfSignedCert() (tls.Certificate, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
// Random serial: a fixed serial (1) makes every generated cert collide in a
|
||||
// browser/OS trust store, so a previously-accepted cert can't be replaced.
|
||||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{Organization: []string{"urania-nadir"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
return tls.Certificate{Certificate: [][]byte{derBytes}, PrivateKey: priv}, nil
|
||||
}
|
||||
|
||||
// generateAndSaveCert generates a self-signed certificate and private key,
|
||||
// and saves them as PEM files.
|
||||
func generateAndSaveCert(certPath, keyPath string, hostname string) error {
|
||||
@@ -75,7 +30,7 @@ func generateAndSaveCert(certPath, keyPath string, hostname string) error {
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{Organization: []string{"nadir-agent-tls"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(3650 * 24 * time.Hour), // 10 years
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 year
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
|
||||
Reference in New Issue
Block a user