From 7ce2b2aa2b7734a6c2afdb26a5d76dcae04b5409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sat, 10 Apr 2021 11:24:10 +0200 Subject: [PATCH] Add autocert --- apiary.toml | 13 +++++++++--- cmd/apiary.go | 21 ++++++++++++++++++- config/config.go | 15 +++++++++++--- go.mod | 1 + go.sum | 6 +++++- honeypot/server.go | 1 - web/server.go | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 97 insertions(+), 11 deletions(-) diff --git a/apiary.toml b/apiary.toml index 7102dc3..72f60b4 100644 --- a/apiary.toml +++ b/apiary.toml @@ -8,7 +8,8 @@ Type = "memory" [Store.Postgres] # Connection string for postgres # Must be set if using Store.Type = "postgres" -DSN = "postgresql://film:film@10.69.10.130:5432/film?sslmode=disable" +DSN = "postgresql://user:password@example.org:5432/apiary" + [Honeypot] # Path to SSH host key # If empty, a new one will be generated each time the service starts @@ -36,12 +37,18 @@ ListenAddr = ":8080" [Frontend.Autocert] # Enable using letsencrypt for automatic certificates +# When enabled Frontend.ListenAddr will be ignored, and the server +# will listen to 443 # Default: false Enable = false +# Email address for certificate owner +Email = "" # Domains to use for certificates. Required when using autocert. # Default: "" -Domains = "example.org" +Domains = ["example.org"] # Dir where certificates are cached. # Default: "/tmp" CacheDir = "/var/apiary/certs" - +# Redirect HTTP to HTTPS +# Default: true +RedirectHTTP = true diff --git a/cmd/apiary.go b/cmd/apiary.go index d503b21..bdb01ba 100644 --- a/cmd/apiary.go +++ b/cmd/apiary.go @@ -17,6 +17,7 @@ import ( "github.uio.no/torjus/apiary/web" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/crypto/acme/autocert" ) func main() { @@ -50,8 +51,10 @@ func ActionServe(c *cli.Context) error { return err } + // Setup logging loggers := setupLoggers(cfg) + // Setup store var s store.LoginAttemptStore switch cfg.Store.Type { case "MEMORY", "memory": @@ -69,16 +72,32 @@ func ActionServe(c *cli.Context) error { return fmt.Errorf("Invalid store configured") } + // Setup honeypot hs, err := honeypot.NewHoneypotServer(cfg.Honeypot, s) if err != nil { return err } hs.Logger = loggers.honeypotLogger + // Setup webserver web := web.NewServer(cfg.Frontend, hs, s) web.AccessLogger = loggers.webAccessLogger web.ServerLogger = loggers.webServerLogger + if cfg.Frontend.Autocert.Enable { + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(cfg.Frontend.Autocert.Domains...), + Email: cfg.Frontend.Autocert.Email, + } + if cfg.Frontend.Autocert.CacheDir != "" { + certManager.Cache = autocert.DirCache(cfg.Frontend.Autocert.CacheDir) + } + tlsConfig := certManager.TLSConfig() + web.TLSConfig = tlsConfig + } + + // Setup interrupt handling interruptChan := make(chan os.Signal, 1) signal.Notify(interruptChan, os.Interrupt) @@ -103,7 +122,7 @@ func ActionServe(c *cli.Context) error { // Start web server go func() { loggers.rootLogger.Info("Starting web server") - if err := web.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := web.StartServe(); err != nil && err != http.ErrServerClosed { loggers.rootLogger.Warnw("Web server returned error", "error", err) } }() diff --git a/config/config.go b/config/config.go index 4bdfaf4..68761f8 100644 --- a/config/config.go +++ b/config/config.go @@ -30,9 +30,18 @@ type HoneypotConfig struct { } type FrontendConfig struct { - ListenAddr string `toml:"ListenAddr"` - LogLevel string `toml:"LogLevel"` - AccessLogEnable bool `toml:"AccessLogEnable"` + ListenAddr string `toml:"ListenAddr"` + LogLevel string `toml:"LogLevel"` + AccessLogEnable bool `toml:"AccessLogEnable"` + Autocert FrontendAutocertConfig `toml:"Autocert"` +} + +type FrontendAutocertConfig struct { + Enable bool `toml:"Enable"` + Email string `toml:"Email"` + Domains []string `toml:"Domains"` + CacheDir string `toml:"CacheDir"` + RedirectHTTP bool `toml:"RedirectHTTP"` } func FromReader(r io.Reader) (Config, error) { diff --git a/go.mod b/go.mod index d26dd73..f735684 100644 --- a/go.mod +++ b/go.mod @@ -16,5 +16,6 @@ require ( github.com/urfave/cli/v2 v2.3.0 go.uber.org/zap v1.13.0 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect ) diff --git a/go.sum b/go.sum index b6238a1..f22fee4 100644 --- a/go.sum +++ b/go.sum @@ -400,6 +400,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -430,14 +432,16 @@ golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= diff --git a/honeypot/server.go b/honeypot/server.go index ea32878..a068147 100644 --- a/honeypot/server.go +++ b/honeypot/server.go @@ -55,7 +55,6 @@ func NewHoneypotServer(cfg config.HoneypotConfig, store store.LoginAttemptStore) } hs.sshServer.AddHostKey(signer) - } return &hs, nil diff --git a/web/server.go b/web/server.go index 86a3a5a..c6ac556 100644 --- a/web/server.go +++ b/web/server.go @@ -18,11 +18,14 @@ import ( "github.uio.no/torjus/apiary/honeypot/store" "github.uio.no/torjus/apiary/models" "go.uber.org/zap" + "golang.org/x/crypto/acme/autocert" ) type Server struct { http.Server + cfg config.FrontendConfig + store store.LoginAttemptStore ServerLogger *zap.SugaredLogger @@ -33,7 +36,8 @@ type Server struct { attemptListenersLock sync.RWMutex attemptListeners map[string]chan models.LoginAttempt - streamContext context.Context + streamContext context.Context + httpRedirectServer http.Server } func NewServer(cfg config.FrontendConfig, hs *honeypot.HoneypotServer, store store.LoginAttemptStore) *Server { @@ -41,8 +45,35 @@ func NewServer(cfg config.FrontendConfig, hs *honeypot.HoneypotServer, store sto ServerLogger: zap.NewNop().Sugar(), AccessLogger: zap.NewNop().Sugar(), store: store, + cfg: cfg, + } + + if cfg.Autocert.Enable { + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(cfg.Autocert.Domains...), + Email: cfg.Autocert.Email, + } + if cfg.Autocert.CacheDir != "" { + certManager.Cache = autocert.DirCache(cfg.Autocert.CacheDir) + } + + tlsConfig := certManager.TLSConfig() + s.TLSConfig = tlsConfig + s.RegisterOnShutdown(func() { + timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + s.httpRedirectServer.Shutdown(timeoutCtx) + }) + s.Addr = ":443" + + if cfg.Autocert.RedirectHTTP { + s.httpRedirectServer.Addr = ":80" + s.httpRedirectServer.Handler = certManager.HTTPHandler(nil) + } + } else { + s.Addr = cfg.ListenAddr } - s.Addr = cfg.ListenAddr r := chi.NewRouter() @@ -79,6 +110,22 @@ func NewServer(cfg config.FrontendConfig, hs *honeypot.HoneypotServer, store sto return s } +func (s *Server) StartServe() error { + if s.cfg.Autocert.Enable { + if s.cfg.Autocert.RedirectHTTP { + s.ServerLogger.Debug("Starting HTTP redirect server") + go func() { + if err := s.httpRedirectServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + s.ServerLogger.Warnw("HTTP redirect server returned error", "error", err) + } + }() + } + return s.ListenAndServeTLS("", "") + } else { + return s.ListenAndServe() + } +} + func (s *Server) addAttemptListener() (string, chan models.LoginAttempt) { ch := make(chan models.LoginAttempt) s.attemptListenersLock.Lock()