Files
redoctober/redoctober.go
Kyle Isom 9f39413adb Properly restore delegations.
This change addresses several points:

1. The integration tests didn't verify that delegations could be used
   for decryption following a restore. The integration tests now
   verify this.

2. There was no functionality for clearing persisted delegations if
   needed. The vault admin can now do this via the command line tool.

3. Restoring active delegations wasn't storing the key with the
   delegation. Keys are now serialised properly.

4. [Minor] The MSP package now reports the name of the offending user
   when it can't find a user name in the database.
2016-08-24 13:22:13 -07:00

332 lines
9.5 KiB
Go

// Package redoctober contains the server code for Red October.
//
// Copyright (c) 2013 CloudFlare, Inc.
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/core"
"github.com/coreos/go-systemd/activation"
"github.com/prometheus/client_golang/prometheus"
)
// List of URLs to register and their related functions
var functions = map[string]func([]byte) ([]byte, error){
"/create": core.Create,
"/create-user": core.CreateUser,
"/summary": core.Summary,
"/purge": core.Purge,
"/delegate": core.Delegate,
"/password": core.Password,
"/encrypt": core.Encrypt,
"/re-encrypt": core.ReEncrypt,
"/decrypt": core.Decrypt,
"/owners": core.Owners,
"/modify": core.Modify,
"/export": core.Export,
"/order": core.Order,
"/orderout": core.OrdersOutstanding,
"/orderinfo": core.OrderInfo,
"/ordercancel": core.OrderCancel,
"/restore": core.Restore,
"/reset-persisted": core.ResetPersisted,
"/status": core.Status,
}
type userRequest struct {
rt string // The request type (which will be one of the
// keys of the functions map above
in []byte // Arbitrary input data (depends on the core.*
// function called)
resp chan<- []byte // Channel down which a response is sent (the
// data sent will depend on the core.* function
// called to handle this request)
}
// processRequest handles a single request receive on the JSON API for
// one of the functions named in the functions map above.
func processRequest(requestType string, w http.ResponseWriter, r *http.Request) {
header := w.Header()
header.Set("Content-Type", "application/json")
header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")
fn, ok := functions[requestType]
if !ok {
http.Error(w, "Unknown request", http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := fn(body)
if err != nil {
log.Printf("http.main failed: %s: %s", requestType, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(resp)
}
// NewServer starts an HTTPS server the handles the redoctober JSON
// API. Each of the URIs in the functions map above is setup with a
// separate HandleFunc. Each HandleFunc is an instance of queueRequest
// above.
//
// Returns a valid http.Server handling redoctober JSON requests (and
// its associated listener) or an error
func NewServer(staticPath, addr, caPath string, certPaths, keyPaths []string, useSystemdSocket bool) (*http.Server, net.Listener, error) {
config := &tls.Config{
PreferServerCipherSuites: true,
SessionTicketsDisabled: true,
}
for i, certPath := range certPaths {
cert, err := tls.LoadX509KeyPair(certPath, keyPaths[i])
if err != nil {
return nil, nil, fmt.Errorf("Error loading certificate (%s, %s): %s", certPath, keyPaths[i], err)
}
config.Certificates = append(config.Certificates, cert)
}
config.BuildNameToCertificate()
// If a caPath has been specified then a local CA is being used
// and not the system configuration.
if caPath != "" {
pemCert, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, nil, fmt.Errorf("Error reading %s: %s\n", caPath, err)
}
derCert, _ := pem.Decode(pemCert)
if derCert == nil {
return nil, nil, fmt.Errorf("No PEM data was found in the CA certificate file\n")
}
cert, err := x509.ParseCertificate(derCert.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Error parsing CA certificate: %s\n", err)
}
rootPool := x509.NewCertPool()
rootPool.AddCert(cert)
config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = rootPool
}
var lstnr net.Listener
if useSystemdSocket {
listenFDs, err := activation.Listeners(true)
if err != nil {
log.Fatal(err)
}
if len(listenFDs) != 1 {
log.Fatalf("Unexpected number of socket activation FDs! (%d)", len(listenFDs))
}
lstnr = tls.NewListener(listenFDs[0], config)
} else {
conn, err := net.Listen("tcp", addr)
if err != nil {
return nil, nil, fmt.Errorf("Error starting TCP listener on %s: %s\n", addr, err)
}
lstnr = tls.NewListener(conn, config)
}
mux := http.NewServeMux()
// queue up post URIs
for current := range functions {
// copy this so reference does not get overwritten
requestType := current
mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) {
log.Printf("http.server: endpoint=%s remote=%s", requestType, r.RemoteAddr)
processRequest(requestType, w, r)
})
}
// queue up web frontend
idxHandler := &indexHandler{staticPath}
mux.HandleFunc("/index", idxHandler.handle)
mux.HandleFunc("/", idxHandler.handle)
srv := http.Server{
Addr: addr,
Handler: mux,
TLSConfig: config,
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
}
return &srv, lstnr, nil
}
type indexHandler struct {
staticPath string
}
func (this *indexHandler) handle(w http.ResponseWriter, r *http.Request) {
var body io.ReadSeeker
if this.staticPath != "" {
f, err := os.Open(this.staticPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
body = f
} else {
body = bytes.NewReader([]byte(indexHtml))
}
header := w.Header()
header.Set("Content-Type", "text/html")
header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")
// If the server isn't HTTPS worthy, the HSTS header won't be honored.
http.ServeContent(w, r, "index.html", time.Now(), body)
}
// initPrometheus starts a goroutine with a Prometheus listener that
// listens on localhost:metricsPort. If the Prometheus handler can't
// be started, a log.Fatal call is made.
func initPrometheus() {
srv := &http.Server{
Addr: net.JoinHostPort(cfg.Metrics.Host, cfg.Metrics.Port),
Handler: prometheus.Handler(),
}
log.Printf("metrics.init start: addr=%s", srv.Addr)
go func() {
log.Fatal(srv.ListenAndServe())
}()
}
const usage = `Usage:
redoctober -static <path> -vaultpath <path> -addr <addr> -certs <path1>[,<path2>,...] -keys <path1>[,<path2>,...] [-ca <path>]
single-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert.pem -keys cert.key
multi-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert1.pem,cert2.pem -keys cert1.key,cert2.key
`
var (
cfg, cli *config.Config
confFile string
vaultPath string
)
const (
defaultAddr = "localhost:8080"
defaultMetricsHost = "localhost"
defaultMetricsPort = "8081"
)
func init() {
// cli contains the configuration set by the command line
// options, and cfg is the actual Red October config.
cli = config.New()
cfg = config.New()
cli.Server.Addr = defaultAddr
cli.Metrics.Host = defaultMetricsHost
cli.Metrics.Port = defaultMetricsPort
flag.Usage = func() {
fmt.Fprint(os.Stderr, "main usage dump\n")
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(2)
}
flag.StringVar(&confFile, "f", "", "path to config file")
flag.StringVar(&cli.Server.Addr, "addr", cli.Server.Addr,
"Server and port separated by :")
flag.StringVar(&cli.Server.CAPath, "ca", cli.Server.CAPath,
"Path of TLS CA for client authentication (optional)")
flag.StringVar(&cli.Server.CertPaths, "certs", cli.Server.CertPaths,
"Path(s) of TLS certificate in PEM format, comma-separated")
flag.StringVar(&cli.HipChat.Host, "hchost", cli.HipChat.Host,
"Hipchat Url Base (ex: hipchat.com)")
flag.StringVar(&cli.HipChat.APIKey, "hckey", cli.HipChat.APIKey,
"Hipchat API Key")
flag.StringVar(&cli.HipChat.Room, "hcroom", cli.HipChat.Room,
"Hipchat Room ID")
flag.StringVar(&cli.Server.KeyPaths, "keys", cli.Server.KeyPaths,
"Comma-separated list of PEM-encoded TLS private keys in the same order as certs")
flag.StringVar(&cli.Metrics.Host, "metrics-host", cli.Metrics.Host,
"The `host` the metrics endpoint should listen on.")
flag.StringVar(&cli.Metrics.Port, "metrics-port", cli.Metrics.Port,
"The `port` the metrics endpoint should listen on.")
flag.StringVar(&cli.UI.Root, "rohost", cli.UI.Root, "RedOctober URL Base (ex: localhost:8080)")
flag.StringVar(&cli.UI.Static, "static", cli.UI.Static,
"Path to override built-in index.html")
flag.BoolVar(&cli.Server.Systemd, "systemdfds", cli.Server.Systemd,
"Use systemd socket activation to listen on a file. Useful for binding privileged sockets.")
flag.StringVar(&vaultPath, "vaultpath", "diskrecord.json", "Path to the the disk vault")
flag.Parse()
}
//go:generate go run generate.go
func main() {
var err error
if confFile != "" {
cfg, err = config.Load(confFile)
if err != nil {
log.Fatal(err)
}
} else {
cfg = cli
}
if vaultPath == "" || !cfg.Valid() {
if !cfg.Valid() {
fmt.Fprintf(os.Stderr, "Invalid config.\n")
}
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(2)
}
if err := core.Init(vaultPath, cfg); err != nil {
log.Fatal(err)
}
initPrometheus()
cpaths := strings.Split(cfg.Server.CertPaths, ",")
kpaths := strings.Split(cfg.Server.KeyPaths, ",")
s, l, err := NewServer(cfg.UI.Static, cfg.Server.Addr, cfg.Server.CAPath,
cpaths, kpaths, cfg.Server.Systemd)
if err != nil {
log.Fatalf("Error starting redoctober server: %s\n", err)
}
log.Printf("http.serve start: addr=%s", cfg.Server.Addr)
log.Fatal(s.Serve(l))
}