config+proxy: add basic authentication

This commit is contained in:
Oliver Gugger 2020-05-29 14:54:15 +02:00
parent b12ed4ca7c
commit b1c7fc9ef6
No known key found for this signature in database
GPG key ID: 8E4256593F177720
2 changed files with 95 additions and 10 deletions

View file

@ -3,6 +3,7 @@ package shushtar
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
@ -18,14 +19,23 @@ import (
"github.com/mwitkow/go-conntrack/connhelpers"
)
const (
defaultHTTPSListen = "127.0.0.1:8443"
uiPasswordMinLength = 8
)
// Config is the main configuration struct of shushtar. It contains all config
// items of its enveloping subservers, each prefixed with their daemon's short
// name.
type Config struct {
HTTPSListen string `long:"httpslisten" description:"host:port to listen for incoming HTTP/2 connections on"`
Lnd *lnd.Config `group:"lnd" namespace:"lnd"`
Faraday *faraday.Config `group:"faraday" namespace:"faraday"`
Loop *loopd.Config `group:"loop" namespace:"loop"`
HTTPSListen string `long:"httpslisten" description:"host:port to listen for incoming HTTP/2 connections on"`
UIPassword string `long:"uipassword" description:"the password that must be entered when using the loop UI. use a strong password to protect your node from unauthorized access through the web UI"`
UIPasswordFile string `long:"uipassword_file" description:"same as uipassword but instead of passing in the value directly, read the password from the specified file"`
UIPasswordEnv string `long:"uipassword_env" description:"same as uipassword but instead of passing in the value directly, read the password from the specified environment variable"`
Lnd *lnd.Config `group:"lnd" namespace:"lnd"`
Faraday *faraday.Config `group:"faraday" namespace:"faraday"`
Loop *loopd.Config `group:"loop" namespace:"loop"`
}
// loadLndConfig loads and sanitizes the lnd main configuration and hooks up all
@ -114,6 +124,45 @@ func getNetwork(cfg *lncfg.Chain) (string, error) {
}
}
// readUIPassword reads the password for the UI either from the command line
// flag, a file specified or an environment variable.
func readUIPassword(config *Config) error {
// A password is passed in as a command line flag (or config file
// variable) directly.
if len(strings.TrimSpace(config.UIPassword)) > 0 {
config.UIPassword = strings.TrimSpace(config.UIPassword)
return nil
}
// A file that contains the password is specified.
if len(strings.TrimSpace(config.UIPasswordFile)) > 0 {
content, err := ioutil.ReadFile(strings.TrimSpace(
config.UIPasswordFile,
))
if err != nil {
return fmt.Errorf("could not read file %s: %v",
config.UIPasswordFile, err)
}
config.UIPassword = strings.TrimSpace(string(content))
return nil
}
// The name of an environment variable was specified.
if len(strings.TrimSpace(config.UIPasswordEnv)) > 0 {
content := os.Getenv(strings.TrimSpace(config.UIPasswordEnv))
if len(content) == 0 {
return fmt.Errorf("environment variable %s is empty",
config.UIPasswordEnv)
}
config.UIPassword = strings.TrimSpace(content)
return nil
}
return fmt.Errorf("mandatory password for UI not configured. specify " +
"either a password directly or a file or environment " +
"variable that contains the password")
}
func buildTLSConfigForHttp2(config *lnd.Config) (*tls.Config, error) {
tlsCert, _, err := cert.LoadCert(config.TLSCertPath, config.TLSKeyPath)
if err != nil {

View file

@ -3,6 +3,7 @@ package shushtar
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
@ -30,9 +31,11 @@ import (
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gopkg.in/macaroon.v2"
// Import generated go package that contains all static files for the
@ -41,7 +44,6 @@ import (
)
const (
defaultHTTPSListen = "127.0.0.1:8443"
defaultServerTimeout = 10 * time.Second
defaultStartupTimeout = 5 * time.Second
)
@ -51,6 +53,10 @@ var (
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
authError = status.Error(
codes.Unauthenticated, "authentication required",
)
lndDefaultConfig = lnd.DefaultConfig()
faradayDefaultConfig = faraday.DefaultConfig()
loopDefaultConfig = loopd.DefaultConfig()
@ -102,6 +108,15 @@ func (g *Shushtar) Run() error {
return err
}
err = readUIPassword(g.cfg)
if err != nil {
return fmt.Errorf("could not read UI password: %v", err)
}
if len(g.cfg.UIPassword) < uiPasswordMinLength {
return fmt.Errorf("please set a strong password for the UI, "+
"at least %d characters long", uiPasswordMinLength)
}
// Load the configuration, and parse any command line options. This
// function will also set up logging properly.
g.cfg.Lnd, err = loadLndConfig(g.cfg)
@ -416,7 +431,7 @@ func (g *Shushtar) startGrpcWebProxy() error {
// admin macaroon and converts the browser's gRPC web calls into native
// gRPC.
lndGrpcServer, grpcServer, err := buildGrpcWebProxyServer(
g.lndAddr, g.cfg.Lnd,
g.lndAddr, g.cfg.UIPassword, g.cfg.Lnd,
)
if err != nil {
return fmt.Errorf("could not create gRPC web proxy: %v", err)
@ -480,7 +495,7 @@ func (g *Shushtar) startGrpcWebProxy() error {
// buildGrpcWebProxyServer creates a gRPC server that will serve gRPC web to the
// browser and translate all incoming gRPC web calls into native gRPC that are
// then forwarded to lnd's RPC interface.
func buildGrpcWebProxyServer(lndAddr string,
func buildGrpcWebProxyServer(lndAddr, uiPassword string,
config *lnd.Config) (*grpcweb.WrappedGrpcServer, *grpc.Server, error) {
// Apply gRPC-wide changes.
@ -489,6 +504,13 @@ func buildGrpcWebProxyServer(lndAddr string,
config.LogWriter, GrpcLogSubsystem,
))
// The gRPC web calls are protected by HTTP basic auth which is defined
// by base64(username:password). Because we only have a password, we
// just use base64(password:password).
basicAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(
"%s:%s", uiPassword, uiPassword,
)))
// Setup the connection to lnd. GRPC web has a few kinks that need to be
// addressed with a custom director that just takes care of a few HTTP
// header fields.
@ -496,7 +518,7 @@ func buildGrpcWebProxyServer(lndAddr string,
if err != nil {
return nil, nil, fmt.Errorf("could not dial lnd: %v", err)
}
director := newDirector(backendConn)
director := newDirector(backendConn, basicAuth)
// Set up the final gRPC server that will serve gRPC web to the browser
// and translate all incoming gRPC web calls into native gRPC that are
@ -515,16 +537,30 @@ func buildGrpcWebProxyServer(lndAddr string,
// newDirector returns a new director function that fixes some common known
// issues when using gRPC web from the browser.
func newDirector(backendConn *grpc.ClientConn) proxy.StreamDirector {
func newDirector(backendConn *grpc.ClientConn,
basicAuth string) proxy.StreamDirector {
return func(ctx context.Context, fullMethodName string) (context.Context,
*grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
mdCopy := md.Copy()
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
return nil, nil, authError
}
authHeaderParts := strings.Split(authHeaders[0], " ")
if len(authHeaderParts) != 2 {
return nil, nil, authError
}
if authHeaderParts[1] != basicAuth {
return nil, nil, authError
}
// If this header is present in the request from the web client,
// the actual connection to the backend will not be established.
// https://github.com/improbable-eng/grpc-web/issues/568
mdCopy := md.Copy()
delete(mdCopy, "connection")
outCtx := metadata.NewOutgoingContext(ctx, mdCopy)