Files
pancho horrillo 1e63f3c104 feat: Control API uses automatic cross-pinning mTLS (Closes #119)
. kapow server generates on startup a pair of certificates
that will use to secure communications to its control server.
It will communicate the server and client certificates as well
as the client private key to the init programs it launches,
via environment variables.

. kapow server now understands a new flag --control-reachable-addr
which accepts either a IP address or a DNS name, that can be used
to ensure that the generated server certificate will be appropiate
in case the control server must be accessed from something other
than localhost.

Co-authored-by: Roberto Abdelkader Martínez Pérez <robertomartinezp@gmail.com>
2021-03-12 17:24:17 +01:00

201 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
"github.com/BBVA/kapow/internal/certs"
"github.com/BBVA/kapow/internal/logger"
"github.com/BBVA/kapow/internal/server"
)
func banner() {
fmt.Fprintln(os.Stderr, `
%% %%%%
%%% %%%
%% %%% %%%
%%%%%%% %%% %%% %%% %%%
*%% %%%%%%%%%%%%%%% %%%% %%% %%
%% %%%%%%%%%. %%% %%%% %%% %%%%%%%%
%%%% %%% %%% %%% %%% %%%%%% %%%%
%%% %%% %%%%%% %%% %%%% %%% %%%% %%%% %%% %%%
%%% %%% %% %%% %%%%% %%%%% %%%% %%%
%%% %%% %% %%%%%%%%% %%%%%%%%%%
%%%%%% %%% %%%%%% %%%
%%% %%%%% %% %%%%%%
%%% %%%%%%%
%%%%
% If you can script it, you can HTTP it.
`)
}
// ServerCmd is the command line interface for kapow server
var ServerCmd = &cobra.Command{
Use: "server [optional flags] [optional init program(s)]",
Short: "Start a kapow server",
Long: `Start a Kapow server with a client interface, a data interface and an
admin interface`,
PreRunE: validateServerCommandArguments,
Run: func(cmd *cobra.Command, args []string) {
var sConf server.ServerConfig = server.ServerConfig{}
sConf.UserBindAddr, _ = cmd.Flags().GetString("bind")
sConf.ControlBindAddr, _ = cmd.Flags().GetString("control-bind")
sConf.DataBindAddr, _ = cmd.Flags().GetString("data-bind")
controlReachableAddr, _ := cmd.Flags().GetString("control-reachable-addr")
sConf.CertFile, _ = cmd.Flags().GetString("certfile")
sConf.KeyFile, _ = cmd.Flags().GetString("keyfile")
sConf.ClientAuth, _ = cmd.Flags().GetBool("clientauth")
sConf.ClientCaFile, _ = cmd.Flags().GetString("clientcafile")
sConf.Debug, _ = cmd.Flags().GetBool("debug")
sConf.ControlServerCert = certs.GenCert("control_server", extractHost(controlReachableAddr), true)
sConf.ControlClientCert = certs.GenCert("control_client", "", false)
// Set environment variables KAPOW_DATA_URL and KAPOW_CONTROL_URL only if they aren't set so we don't overwrite user's preferences
if _, exist := os.LookupEnv("KAPOW_DATA_URL"); !exist {
os.Setenv("KAPOW_DATA_URL", "http://"+sConf.DataBindAddr)
}
if _, exist := os.LookupEnv("KAPOW_CONTROL_URL"); !exist {
os.Setenv("KAPOW_CONTROL_URL", "https://"+controlReachableAddr)
}
banner()
server.StartServer(sConf)
for _, path := range args {
go Run(
path,
sConf.Debug,
sConf.ControlServerCert.SignedCertPEMBytes(),
sConf.ControlClientCert.SignedCertPEMBytes(),
sConf.ControlClientCert.PrivateKeyPEMBytes(),
)
}
select {}
},
}
func extractHost(s string) string {
i := strings.LastIndex(s, ":")
s = s[:i]
l := len(s) - 1
if s[0] == '[' && s[l] == ']' {
s = s[1:l]
}
return s
}
func init() {
ServerCmd.Flags().String("bind", "0.0.0.0:8080", "IP address and port to bind the user interface to")
ServerCmd.Flags().String("control-bind", "localhost:8081", "IP address and port to bind the control interface to")
ServerCmd.Flags().String("data-bind", "localhost:8082", "IP address and port to bind the data interface to")
ServerCmd.Flags().String("control-reachable-addr", "localhost:8081", "address (incl. port) through which the control interface can be reached (from the client's point of view)")
ServerCmd.Flags().String("certfile", "", "Cert file to serve thru https")
ServerCmd.Flags().String("keyfile", "", "Key file to serve thru https")
ServerCmd.Flags().Bool("clientauth", false, "Activate client mutual tls authentication")
ServerCmd.Flags().String("clientcafile", "", "Cert file to validate client certificates")
ServerCmd.Flags().Bool("debug", false, "Activate debug mode for script executions to standard output")
}
func validateServerCommandArguments(cmd *cobra.Command, args []string) error {
cert, _ := cmd.Flags().GetString("certfile")
key, _ := cmd.Flags().GetString("keyfile")
cliAuth, _ := cmd.Flags().GetBool("clientauth")
if (cert == "") != (key == "") {
return errors.New("expected both or neither (certfile and keyfile)")
}
if cert == "" {
// If we don't serve thru https client authentication can't be enabled
if cliAuth {
return errors.New("Client authentication can't be active in a non https server")
}
}
return nil
}
func Run(
path string,
debug bool,
controlServerCertPEM,
controlClientCertPEM,
controlClientCertPrivKeyPEM []byte,
) {
logger.L.Printf("Running init program %+q", path)
cmd := BuildCmd(path)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_SERVER_CERT=%s", controlServerCertPEM))
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_CERT=%s", controlClientCertPEM))
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_KEY=%s", controlClientCertPrivKeyPEM))
var wg sync.WaitGroup
if debug {
if stdout, err := cmd.StdoutPipe(); err == nil {
wg.Add(1)
go logPipe(path, "stdout", stdout, &wg)
}
if stderr, err := cmd.StderrPipe(); err == nil {
wg.Add(1)
go logPipe(path, "stderr", stderr, &wg)
}
}
err := cmd.Start()
if err != nil {
logger.L.Fatalf("Unable to run init program %+q: %s", path, err)
}
wg.Wait()
err = cmd.Wait()
if err != nil {
logger.L.Printf("Init program exited with error: %s", err)
} else {
logger.L.Printf("Init program %+q finished OK", path)
}
}
func logPipe(path, name string, pipe io.ReadCloser, wg *sync.WaitGroup) {
defer wg.Done()
in := bufio.NewScanner(pipe)
for in.Scan() {
logger.L.Printf("%+q (%s): %s", path, name, in.Text())
}
if err := in.Err(); err != nil {
logger.L.Printf("Error reading from %+qs %s: %s", path, name, err)
}
}