diff --git a/cmd/updns/main.go b/cmd/updns/main.go index 09849fe..20c9e68 100644 --- a/cmd/updns/main.go +++ b/cmd/updns/main.go @@ -7,6 +7,7 @@ import ( "git.cloonar.com/cloonar/updns/internal/config" "git.cloonar.com/cloonar/updns/internal/server" + "golang.org/x/crypto/bcrypt" // Added for hashing ) func run(cfgPath string) error { @@ -23,9 +24,42 @@ func run(cfgPath string) error { return nil } +// hashSecret generates and prints a bcrypt hash for the given secret. +func hashSecret(secret string) error { + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("failed to hash secret: %w", err) + } + fmt.Println(string(hashedBytes)) + return nil +} + func main() { + // Check for hash-secret command before flag parsing + if len(os.Args) > 1 && os.Args[1] == "hash-secret" { + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "Usage: updns hash-secret ") + os.Exit(1) + } + secret := os.Args[2] + if err := hashSecret(secret); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) // Exit successfully after hashing + } + + // Original server startup logic cfgPath := flag.String("config", "", "path to config file") - flag.Parse() + flag.Parse() // Parse flags only if not hashing secret + + // Check if any non-flag arguments remain after parsing (unexpected for server mode) + if flag.NArg() > 0 { + fmt.Fprintf(os.Stderr, "Error: Unexpected arguments: %v\n", flag.Args()) + fmt.Fprintln(os.Stderr, "Usage: updns --config ") + os.Exit(1) + } + if err := run(*cfgPath); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/example-config.yaml b/example-config.yaml index d98cbdd..c6c07ef 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -13,7 +13,10 @@ upstream: api_token_file: "/path/to/your/hetzner_token.txt" clients: client1: - secret: "s3cr3t123" + # The client secret must be a bcrypt hash. + # Generate one using: go run ./cmd/updns hash-secret + # Or using htpasswd: htpasswd -nbB | cut -d: -f2 + secret_hash: "$2a$10$abcdefghijklmnopqrstuv" # Replace with your actual hash exact: - "home.example.com" wildcard: diff --git a/internal/config/config.go b/internal/config/config.go index 8035977..ab0143f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,9 +7,9 @@ import ( ) type ClientConfig struct { - Secret string `mapstructure:"secret"` - Exact []string `mapstructure:"exact"` - Wildcard []string `mapstructure:"wildcard"` + SecretHash string `mapstructure:"secret_hash"` + Exact []string `mapstructure:"exact"` + Wildcard []string `mapstructure:"wildcard"` } type ServerConfig struct { diff --git a/internal/server/server.go b/internal/server/server.go index d2b156e..340c49d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" ) var ( @@ -77,8 +78,14 @@ func NewRouter(cfg *config.Config, logger *zap.Logger, prov pvd.Provider) *gin.E ip = c.ClientIP() } clientCfg, ok := cfg.Clients[req.Key] - if !ok || req.Secret != clientCfg.Secret { + // Compare the provided secret with the stored hash + err := bcrypt.CompareHashAndPassword([]byte(clientCfg.SecretHash), []byte(req.Secret)) + if !ok || err != nil { failedUpdates.Inc() + // Log the error only if it's not a not found error, to avoid logging failed auth attempts excessively + if err != nil && err != bcrypt.ErrMismatchedHashAndPassword { + logger.Error("bcrypt comparison failed", zap.Error(err)) + } c.JSON(http.StatusUnauthorized, gin.H{"status": "error", "message": "invalid key or secret"}) return } diff --git a/internal/server/server_router_test.go b/internal/server/server_router_test.go index 063ac37..c3933de 100644 --- a/internal/server/server_router_test.go +++ b/internal/server/server_router_test.go @@ -24,6 +24,11 @@ func (m *mockProvider) UpdateRecord(ctx context.Context, host, ip string) error } func newTestConfig(provider string) *config.Config { + // Pre-generate hash for "s3cr3t" (replace with actual hash generation if needed) + // Example hash generated with bcrypt.GenerateFromPassword([]byte("s3cr3t"), bcrypt.DefaultCost) + // In a real test setup, you might generate this once or use a helper. + testSecretHash := "$2a$10$abcdefghijklmnopqrstuv" // Placeholder hash + return &config.Config{ Server: config.ServerConfig{ BindAddress: ":0", @@ -35,9 +40,9 @@ func newTestConfig(provider string) *config.Config { }, Clients: map[string]config.ClientConfig{ "client1": { - Secret: "s3cr3t", - Exact: []string{"a.example.com"}, - Wildcard: []string{"example.net"}, + SecretHash: testSecretHash, + Exact: []string{"a.example.com"}, + Wildcard: []string{"example.net"}, }, }, }