Automatic TLS certificate'ing with Let's Encrypt

To use Let's Encrypt:
- Set `acme_domain_name` to the domain name of the machine.
- Set both `listen` and `listen_https` to the ports Flamenco Manager
  should be listening to. By default these are `:8080` and `:8443`.
- Configure your firewall or user-facing proxy to forward ports 80 and
  443 to respectively 8080 and 8443.

Other changes:
- Added setting `listen_https` which is used for serving HTTPS traffic
  (default `:8443`). If you are using the `tlskey`/`tlscert` settings, you
  need to move `listen` to `listen_https`.
- Changed the default value for `listen` to `:8080` (was `:8083`).

The changes to the default were somewhat necessary to get to more
standard port numbers; it would be silly to add the standard port number
8443 and still keep using the nonstandard 8083.

A new webserver wrapper was introduced that manages both the HTTP and
HTTPS servers as a single unit. When using ACME/Let's Encrypt it is
necessary to have both HTTP (for the ACME web authentication) and HTTPS
(for regular traffic). All other HTTP traffic is redirected to HTTPS on
port 443. This does *not* redirect to the configured `listen_https` port
because firewall-based redirection or reverse proxies may be in use.
Actually, this is recommended because then Flamenco Manager doesn't need
to be run as root.
This commit is contained in:
2019-04-19 13:53:32 +02:00
parent 392decedb4
commit 715faa6852
10 changed files with 282 additions and 39 deletions

176
httpserver/httpserver.go Normal file
View File

@@ -0,0 +1,176 @@
package httpserver
import (
"context"
"net/http"
"sync"
"time"
"github.com/armadillica/flamenco-manager/flamenco"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/acme/autocert"
)
// Constants for the HTTP servers.
const (
ReadTimeout = 15 * time.Second
)
// Server acts as a http.Server
type Server interface {
Shutdown(ctx context.Context)
ListenAndServe() error
Done() <-chan struct{}
}
// Combined has one or two HTTP servers.
type Combined struct {
httpServer *http.Server
httpsServer *http.Server
tlsKey string
tlsCert string
expectShutdown bool
mutex sync.Mutex
shutdownComplete chan struct{} // closed when ListenAndServe stops serving.
}
// New returns a new HTTP server that can handle HTTP, HTTPS, and both for ACME.
func New(config flamenco.Conf, handler http.Handler) Server {
server := Combined{
mutex: sync.Mutex{},
shutdownComplete: make(chan struct{}),
}
switch {
case config.HasCustomTLS():
logrus.WithFields(logrus.Fields{
"tlscert": config.TLSCert,
"tlskey": config.TLSKey,
"listen_https": config.ListenHTTPS,
}).Info("creating HTTPS-enabled server")
server.httpsServer = &http.Server{
Addr: config.ListenHTTPS,
Handler: handler,
ReadTimeout: ReadTimeout,
}
server.tlsKey = config.TLSKey
server.tlsCert = config.TLSCert
case config.ACMEDomainName != "":
logrus.WithFields(logrus.Fields{
"acme_domain_name": config.ACMEDomainName,
"listen": config.Listen,
"listen_https": config.ListenHTTPS,
}).Info("creating ACME/Let's Encrypt enabled server")
mgr := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(config.ACMEDomainName),
Cache: autocert.DirCache("tlscerts"),
}
server.httpServer = &http.Server{
Addr: config.Listen,
Handler: mgr.HTTPHandler(nil),
}
server.httpsServer = &http.Server{
Addr: config.ListenHTTPS,
Handler: handler,
ReadTimeout: ReadTimeout,
TLSConfig: mgr.TLSConfig(),
}
default:
logrus.WithField("listen", config.Listen).Info("creating insecure server")
server.httpServer = &http.Server{
Addr: config.Listen,
Handler: handler,
ReadTimeout: ReadTimeout,
}
}
return &server
}
// Shutdown shuts down both HTTP and HTTPS server.
func (s *Combined) Shutdown(ctx context.Context) {
s.mutex.Lock()
s.expectShutdown = true
s.mutex.Unlock()
if s.httpServer != nil {
s.httpServer.Shutdown(ctx)
}
if s.httpsServer != nil {
s.httpsServer.Shutdown(ctx)
}
}
// Done returns a channel that is closed when the server is done serving.
func (s *Combined) Done() <-chan struct{} {
return s.shutdownComplete
}
func (s *Combined) mustBeFresh() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.expectShutdown {
panic("this HTTP server was already shut down, unable to restart")
}
}
// ListenAndServe listens on both HTTP and HTTPS servers.
func (s *Combined) ListenAndServe() error {
s.mustBeFresh()
defer close(s.shutdownComplete)
var httpError, httpsError error
wg := sync.WaitGroup{}
if s.httpServer != nil {
wg.Add(1)
go func() {
logger := logrus.WithField("listen", s.httpServer.Addr)
logger.Debug("starting HTTP server")
err := s.httpServer.ListenAndServe()
s.mutex.Lock()
defer s.mutex.Unlock()
if !s.expectShutdown {
logger.WithError(httpError).Error("HTTP server unexpectedly stopped")
httpError = err
}
wg.Done()
}()
}
if s.httpsServer != nil {
wg.Add(1)
go func() {
logger := logrus.WithField("listen_https", s.httpsServer.Addr)
logger.Debug("starting HTTPS server")
err := s.httpsServer.ListenAndServeTLS(s.tlsCert, s.tlsKey)
s.mutex.Lock()
defer s.mutex.Unlock()
if !s.expectShutdown {
logger.WithError(err).Error("HTTPS server unexpectedly stopped")
httpsError = err
}
wg.Done()
}()
}
wg.Wait()
// We can only return one error.
if httpsError != nil {
return httpsError
}
return httpError
}