v0.21.0 scram password auth, cred and token management, browser tls-terminating proxy, viewer client
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
1 parent
f192c26fa2
commit
9ab8c356d2
60 files changed
+6791
-747
No files matched your search
+33
-50
@@ -172,15 +172,6 @@ type PluginSourceConfig struct {
|
||||
ConfigFile string `toml:"config_file,omitempty"` // TODO: support for include/source mechanism for nested config
|
||||
}
|
||||
|
||||
// // SourceConfig is a polymorphic struct representing a single data source
|
||||
// type SourceConfig struct {
|
||||
// Type string `toml:"type"`
|
||||
//
|
||||
// // Polymorphic - only one populated based on type
|
||||
// File *FileSourceOptions `toml:"file,omitempty"`
|
||||
// Console *ConsoleSourceOptions `toml:"console,omitempty"`
|
||||
// }
|
||||
|
||||
// NullSourceOptions defines settings for a null source (no configuration needed)
|
||||
type NullSourceOptions struct{}
|
||||
|
||||
@@ -219,7 +210,6 @@ type TCPChainSourceOptions struct {
|
||||
HelloTimeoutMS int64 `toml:"hello_timeout_ms"` // preamble deadline
|
||||
TrustNode bool `toml:"trust_node"` // false: force node label from remote address
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// HTTPChainSourceOptions defines settings for a stdlib HTTP listener ingesting
|
||||
@@ -234,7 +224,6 @@ type HTTPChainSourceOptions struct {
|
||||
ReadTimeoutMS int64 `toml:"read_timeout_ms"` // full request read deadline
|
||||
TrustNode bool `toml:"trust_node"` // false: force node label from remote address
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// --- Sink Options ---
|
||||
@@ -247,15 +236,6 @@ type PluginSinkConfig struct {
|
||||
ConfigFile string `toml:"config_file,omitempty"` // TODO: support for include/source mechanism for nested config
|
||||
}
|
||||
|
||||
// // SinkConfig is a polymorphic struct representing a single data sink
|
||||
// type SinkConfig struct {
|
||||
// Type string `toml:"type"`
|
||||
//
|
||||
// // Polymorphic - only one populated based on type
|
||||
// Console *ConsoleSinkOptions `toml:"console,omitempty"`
|
||||
// File *FileSinkOptions `toml:"file,omitempty"`
|
||||
// }
|
||||
|
||||
// NullSinkOptions defines settings for a null sink (no configuration needed)
|
||||
type NullSinkOptions struct{}
|
||||
|
||||
@@ -289,7 +269,6 @@ type TCPSinkOptions struct {
|
||||
KeepAlivePeriodMS int64 `toml:"keep_alive_period_ms"`
|
||||
MaxConnections int64 `toml:"max_connections"` // 0 = unlimited
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// HTTPSinkOptions defines settings for an HTTP SSE server sink
|
||||
@@ -303,8 +282,9 @@ type HTTPSinkOptions struct {
|
||||
ClientBufferSize int64 `toml:"client_buffer_size"` // per-client send queue
|
||||
WriteTimeoutMS int64 `toml:"write_timeout_ms"` // per-SSE-write deadline, 0 = none
|
||||
MaxConnections int64 `toml:"max_connections"` // 0 = unlimited
|
||||
LoginPage bool `toml:"login_page"` // GET /auth/login, needs auth.trusted_proxies
|
||||
ViewerPage bool `toml:"viewer_page"` // GET /auth/view, needs login_page
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// TCPChainSinkOptions defines settings for a stdlib TCP client forwarding
|
||||
@@ -322,7 +302,6 @@ type TCPChainSinkOptions struct {
|
||||
KeepAlivePeriodMS int64 `toml:"keep_alive_period_ms"`
|
||||
KeepAlive bool `toml:"keep_alive"`
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// HTTPChainSinkOptions defines settings for a stdlib HTTP client posting
|
||||
@@ -341,49 +320,53 @@ type HTTPChainSinkOptions struct {
|
||||
BackoffMinMS int64 `toml:"backoff_min_ms"`
|
||||
BackoffMaxMS int64 `toml:"backoff_max_ms"`
|
||||
Auth *AuthOptions `toml:"auth"`
|
||||
// Future: password auth block
|
||||
}
|
||||
|
||||
// --- Auth Options ---
|
||||
|
||||
// AuthOptions defines certificate-based authorization for network plugins.
|
||||
// It sits beside `tls` rather than inside it: TLS answers "is this channel
|
||||
// private and does the peer chain to a CA", auth answers "may *this* peer do
|
||||
// *this*". One shape serves both roles:
|
||||
// - Listeners (tcp/http sinks, tcp_chain/http_chain sources) authorize the
|
||||
// peer's client certificate; type "mtls" requires tls.client_auth.
|
||||
// - Dialers (tcp_chain/http_chain sinks) pin the server's identity beyond
|
||||
// hostname verification.
|
||||
// AuthOptions selects how a network plugin authenticates its peer. It sits
|
||||
// beside `tls`: TLS answers "is this channel private", auth answers "may this
|
||||
// peer do this". Listeners verify (mtls: certificate identity; scram: password
|
||||
// via credentials_file); dialers prove themselves (scram) or pin the server
|
||||
// (mtls). Validation is per role in internal/authz.
|
||||
type AuthOptions struct {
|
||||
// Method: "none" (default, preserves pre-auth behavior) | "mtls"
|
||||
// Method: "none" (default) | "mtls" | "scram"
|
||||
Type string `toml:"type"`
|
||||
|
||||
// Certificate field carrying the identity:
|
||||
// "cn" (default) | "san_dns" | "san_uri" | "san_email"
|
||||
// Certificate field carrying the identity: "cn" (default) | "san_dns" |
|
||||
// "san_uri" | "san_email". Under scram it binds the client certificate to
|
||||
// the user: the field must equal the SCRAM username.
|
||||
Identity string `toml:"identity"`
|
||||
|
||||
// Exact identity matches. Empty Allow *and* AllowPatterns means "any
|
||||
// identity the CA vouches for" - today's behavior, but with the identity
|
||||
// recorded and node binding available.
|
||||
Allow []string `toml:"allow"`
|
||||
|
||||
// RE2 patterns matched against the identity; anchor them yourself
|
||||
// mtls only: exact identities and RE2 patterns (anchor them yourself).
|
||||
// Both empty admits any identity the CA vouches for.
|
||||
Allow []string `toml:"allow"`
|
||||
AllowPatterns []string `toml:"allow_patterns"`
|
||||
|
||||
// Chain sources only: "none" | "assert" | "force" (default "force" when
|
||||
// Type is "mtls"). Overrides trust_node.
|
||||
// Chain sources only: "none" | "assert" | "force" (default "force").
|
||||
// Overrides trust_node.
|
||||
NodeBinding string `toml:"node_binding"`
|
||||
|
||||
// scram listeners: verifier file written by `logwisp auth add-user`, and
|
||||
// the bearer token lifetime on HTTP listeners (default 15 minutes)
|
||||
CredentialsFile string `toml:"credentials_file"`
|
||||
TokenLifetimeMS int64 `toml:"token_lifetime_ms"`
|
||||
|
||||
// scram http sink: addresses or CIDRs of the reverse proxies that end the
|
||||
// browsers' TLS; logins are then unbound and sessions may be cookies
|
||||
TrustedProxies []string `toml:"trusted_proxies"`
|
||||
|
||||
// scram dialers: the identity presented and the file holding its password
|
||||
Username string `toml:"username"`
|
||||
PasswordFile string `toml:"password_file"`
|
||||
}
|
||||
|
||||
// --- TLS Options ---
|
||||
|
||||
// TLSOptions defines transport security for network sources and sinks.
|
||||
// One shape serves both roles so the config block is uniform:
|
||||
// - Listeners (tcp/http sinks, tcp_chain/http_chain sources) use
|
||||
// cert_file/key_file as server identity; client_auth/client_ca_file
|
||||
// require and verify peer certificates (mTLS).
|
||||
// - Dialers (tcp_chain/http_chain sinks) use ca_file/server_name to verify
|
||||
// the server; cert_file/key_file present a client identity (mTLS).
|
||||
// TLSOptions is one shape for both roles. Listeners present cert_file and
|
||||
// key_file and verify clients with client_auth and client_ca_file; dialers
|
||||
// verify the server with ca_file and server_name and may present
|
||||
// cert_file and key_file.
|
||||
type TLSOptions struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ func (m *Manager) Snapshot() (*Config, error) {
|
||||
}
|
||||
cfg := value.(*Config)
|
||||
cfg.ConfigFile = m.path
|
||||
if err := checkFileKeys(m.path); err != nil {
|
||||
return nil, fmt.Errorf("config file %q: %w", m.path, err)
|
||||
}
|
||||
if err := ValidateConfig(cfg); err != nil {
|
||||
return nil, fmt.Errorf("validate configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -228,3 +228,35 @@ port = 9000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A misspelled table path above a plugin's config drops that whole table, so
|
||||
// the file itself is checked too; only config_file is tolerated, as documented.
|
||||
func TestConfigFileRejectsUnknownKeys(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
for key, body := range map[string]string{
|
||||
"pipelines[0].plugin_sinks[0].confg": "[pipelines.plugin_sinks.confg.tls]\nenabled = true",
|
||||
"logging.levle": "[logging]\nlevle = \"debug\"",
|
||||
} {
|
||||
testutil.WriteFile(t, "typo.toml", `config_file = "ignored.toml"
|
||||
[[pipelines]]
|
||||
name = "typo"
|
||||
[[pipelines.plugin_sources]]
|
||||
id = "in"
|
||||
type = "null"
|
||||
[[pipelines.plugin_sinks]]
|
||||
id = "out"
|
||||
type = "tcp"
|
||||
[pipelines.plugin_sinks.config]
|
||||
port = 9000
|
||||
`+body+"\n")
|
||||
m, err := Load([]string{"-c", "typo.toml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = m.Snapshot()
|
||||
m.Close()
|
||||
if err == nil || !strings.Contains(err.Error(), `"`+key+`"`) {
|
||||
t.Errorf("unknown key %s: err = %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
lconfig "github.com/lixenwraith/config"
|
||||
"github.com/lixenwraith/toml"
|
||||
)
|
||||
|
||||
// ValidateConfig validates top-level structure only
|
||||
@@ -119,7 +123,9 @@ func unknownKeys(m map[string]any, t reflect.Type, prefix string) error {
|
||||
var tables []map[string]any
|
||||
switch v := m[key].(type) {
|
||||
case map[string]any:
|
||||
tables = []map[string]any{v}
|
||||
if err := unknownKeys(v, ft, prefix+key+"."); err != nil {
|
||||
return err
|
||||
}
|
||||
case []map[string]any:
|
||||
tables = v
|
||||
case []any:
|
||||
@@ -129,11 +135,30 @@ func unknownKeys(m map[string]any, t reflect.Type, prefix string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, table := range tables {
|
||||
if err := unknownKeys(table, ft, prefix+key+"."); err != nil {
|
||||
for i, table := range tables {
|
||||
if err := unknownKeys(table, ft, fmt.Sprintf("%s%s[%d].", prefix, key, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkFileKeys rejects keys the configuration file declares that Config does
|
||||
// not: a misspelled table path such as plugin_sinks.confg.tls would otherwise
|
||||
// drop the whole table. Plugin config maps are checked by Scan instead.
|
||||
func checkFileKeys(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := toml.NewParser(data).Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(root, "config_file") // runtime metadata, documented as ignored in the file
|
||||
return unknownKeys(root, reflect.TypeOf(Config{}), "")
|
||||
}
|
||||
Reference in new issue
Block a user