Skip to content

Commit 2d1e9ea

Browse files
committed
feat(auth): add auto-TLS and API token authentication
Add local security layer (lnd-inspired) to protect neutrino-api from eavesdropping and unauthorized access by other processes on the same host. - Auto-generate self-signed TLS certificate (EC P-256) on first start - Auto-generate 32-byte random hex API token on first start - Add Bearer token authentication middleware (constant-time comparison) - Add --reset-auth flag to regenerate credentials and clear privacy data - Add --no-auth / NO_AUTH=true for development/regtest (disables TLS+auth) - Add ClearPrivacyData() to purge watched addresses and UTXOs on reset - Set NO_AUTH=true in docker-compose.yml for regtest environment Changelog: Add auto-TLS and API token authentication for local security
1 parent f305d2a commit 2d1e9ea

8 files changed

Lines changed: 800 additions & 5 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ services:
5050
- LOG_LEVEL=debug
5151
- ADD_PEERS=bitcoin:18444
5252
- MAX_PEERS=8
53+
- NO_AUTH=true
5354
ports:
5455
- "8334:8334"
5556
volumes:

neutrino_server/cmd/neutrinod/main.go

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66

77
import (
88
"context"
9+
"crypto/tls"
910
"flag"
1011
"fmt"
1112
"net/http"
@@ -19,6 +20,7 @@ import (
1920
"github.com/gorilla/mux"
2021

2122
"github.com/yourusername/neutrino-api/neutrino_server/internal/api"
23+
"github.com/yourusername/neutrino-api/neutrino_server/internal/auth"
2224
"github.com/yourusername/neutrino-api/neutrino_server/internal/neutrino"
2325
)
2426

@@ -40,6 +42,8 @@ func main() {
4042
prefetchStart := flag.Int("prefetchstart", getEnvInt("PREFETCH_START", 0), "Start height for background filter prefetch")
4143
prefetchLookback := flag.Int("prefetchlookback", getEnvInt("PREFETCH_LOOKBACK", 105120), "When >0 and prefetchstart=0, auto-compute prefetch start as tip minus this many blocks (~2 years default)")
4244
clearnetInitialSync := flag.Bool("clearnet-initial-sync", getEnvBool("CLEARNET_INITIAL_SYNC", true), "Sync block headers over clearnet before switching to Tor (safe: headers are public data)")
45+
noAuth := flag.Bool("no-auth", getEnvBool("NO_AUTH", false), "Disable TLS and token authentication (for development/regtest)")
46+
resetAuth := flag.Bool("reset-auth", false, "Regenerate TLS cert and auth token, clear watched addresses, then exit")
4347
showVersion := flag.Bool("version", false, "Show version and exit")
4448
flag.Parse()
4549

@@ -71,6 +75,40 @@ func main() {
7175
os.Exit(1)
7276
}
7377

78+
// Handle --reset-auth: regenerate credentials, clear privacy data, exit.
79+
if *resetAuth {
80+
if err := handleResetAuth(*dataDir, logger); err != nil {
81+
logger.Errorf("Reset auth failed: %v", err)
82+
os.Exit(1)
83+
}
84+
os.Exit(0)
85+
}
86+
87+
// Set up TLS and auth token (unless --no-auth).
88+
var tlsCfg *tls.Config
89+
var authToken string
90+
91+
if !*noAuth {
92+
tlsConfig, err := auth.LoadOrGenerateTLS(*dataDir, logger)
93+
if err != nil {
94+
logger.Errorf("Failed to set up TLS: %v", err)
95+
os.Exit(1)
96+
}
97+
tlsCfg = tlsConfig.Config
98+
logger.Infof("TLS certificate: %s", tlsConfig.CertPath)
99+
100+
authLogger := backend.Logger("AUTH")
101+
authLogger.SetLevel(level)
102+
authToken, err = auth.LoadOrGenerateToken(*dataDir, authLogger)
103+
if err != nil {
104+
logger.Errorf("Failed to set up auth token: %v", err)
105+
os.Exit(1)
106+
}
107+
logger.Infof("Auth token loaded (clients must present Bearer token)")
108+
} else {
109+
logger.Warnf("Authentication disabled (--no-auth). API is unprotected!")
110+
}
111+
74112
// Create neutrino node
75113
nodeConfig := &neutrino.Config{
76114
Network: *network,
@@ -108,9 +146,17 @@ func main() {
108146

109147
// Set up router
110148
router := mux.NewRouter()
149+
150+
// Register auth middleware before API routes (if enabled).
151+
if authToken != "" {
152+
authLogger := backend.Logger("AUTH")
153+
authLogger.SetLevel(level)
154+
router.Use(auth.Middleware(authToken, authLogger))
155+
}
156+
111157
handler.RegisterRoutes(router)
112158

113-
// Create HTTP server
159+
// Create HTTP(S) server
114160
server := &http.Server{
115161
Addr: *listen,
116162
Handler: router,
@@ -119,11 +165,22 @@ func main() {
119165
IdleTimeout: 60 * time.Second,
120166
}
121167

122-
// Start HTTP server in background
168+
if tlsCfg != nil {
169+
server.TLSConfig = tlsCfg
170+
}
171+
172+
// Start server in background
123173
go func() {
124-
logger.Infof("HTTP server listening on %s", *listen)
125-
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
126-
logger.Errorf("HTTP server error: %v", err)
174+
if tlsCfg != nil {
175+
logger.Infof("HTTPS server listening on %s (TLS enabled)", *listen)
176+
if err := server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
177+
logger.Errorf("HTTPS server error: %v", err)
178+
}
179+
} else {
180+
logger.Infof("HTTP server listening on %s", *listen)
181+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
182+
logger.Errorf("HTTP server error: %v", err)
183+
}
127184
}
128185
}()
129186

@@ -149,6 +206,49 @@ func main() {
149206
logger.Info("Shutdown complete")
150207
}
151208

209+
// handleResetAuth regenerates TLS credentials and auth token, clears
210+
// privacy-sensitive data (watched addresses, UTXOs), and prints the
211+
// new auth token to stdout.
212+
func handleResetAuth(dataDir string, logger btclog.Logger) error {
213+
logger.Info("Resetting auth credentials...")
214+
215+
// Remove existing TLS files so they get regenerated.
216+
for _, name := range []string{auth.TLSCertFilename, auth.TLSKeyFilename, auth.TokenFilename} {
217+
path := fmt.Sprintf("%s/%s", dataDir, name)
218+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
219+
return fmt.Errorf("failed to remove %s: %w", path, err)
220+
}
221+
}
222+
223+
// Generate fresh TLS cert and auth token.
224+
if _, err := auth.LoadOrGenerateTLS(dataDir, logger); err != nil {
225+
return fmt.Errorf("failed to generate TLS: %w", err)
226+
}
227+
token, err := auth.LoadOrGenerateToken(dataDir, logger)
228+
if err != nil {
229+
return fmt.Errorf("failed to generate token: %w", err)
230+
}
231+
232+
// Clear watched addresses and UTXOs from the state database.
233+
storeLogger := logger // reuse same logger for brevity
234+
store, err := neutrino.OpenStateStore(dataDir, storeLogger)
235+
if err != nil {
236+
logger.Warnf("Could not open state store to clear privacy data: %v", err)
237+
} else {
238+
if err := store.ClearPrivacyData(); err != nil {
239+
logger.Warnf("Failed to clear privacy data: %v", err)
240+
} else {
241+
logger.Info("Cleared watched addresses and UTXO set")
242+
}
243+
store.Close()
244+
}
245+
246+
logger.Info("Auth reset complete")
247+
fmt.Printf("New auth token: %s\n", token)
248+
fmt.Printf("TLS certificate: %s/%s\n", dataDir, auth.TLSCertFilename)
249+
return nil
250+
}
251+
152252
// getEnv returns the value of an environment variable or a default value.
153253
func getEnv(key, defaultValue string) string {
154254
if value := os.Getenv(key); value != "" {

0 commit comments

Comments
 (0)