Add config option to enable (m)TLS between Poseidon and Nomad

This commit is contained in:
Jan-Eric Hellenberg
2021-07-27 13:45:46 +02:00
committed by Jan-Eric Hellenberg
parent e2d71a11ad
commit 6a60b6cd89
14 changed files with 134 additions and 98 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ tests/e2e/configuration.yaml
# TLS certificate/key # TLS certificate/key
*.crt *.crt
*.key *.key
*.pem
# trivy artifacts # trivy artifacts
.trivy .trivy

View File

@ -101,7 +101,7 @@ Once configured, all requests to the Nomad API automatically contain a `X-Nomad-
### TLS ### TLS
We highly encourage the use of TLS in this API to increase the security. To enable TLS, set `server.tls` or the corresponding environment variable to true and specify the `server.certfile` and `server.keyfile` options. We highly encourage the use of TLS in this API to increase the security. To enable TLS, set `server.tls.active` or the corresponding environment variable to true and specify the `server.tls.certfile` and `server.tls.keyfile` options.
You can create a self-signed certificate to use with this API using the following command. You can create a self-signed certificate to use with this API using the following command.

View File

@ -24,13 +24,13 @@ var (
func runServer(server *http.Server) { func runServer(server *http.Server) {
log.WithField("address", server.Addr).Info("Starting server") log.WithField("address", server.Addr).Info("Starting server")
var err error var err error
if config.Config.Server.TLS { if config.Config.Server.TLS.Active {
server.TLSConfig = config.TLSConfig server.TLSConfig = config.TLSConfig
log. log.
WithField("CertFile", config.Config.Server.CertFile). WithField("CertFile", config.Config.Server.TLS.CertFile).
WithField("KeyFile", config.Config.Server.KeyFile). WithField("KeyFile", config.Config.Server.TLS.KeyFile).
Debug("Using TLS") Debug("Using TLS")
err = server.ListenAndServeTLS(config.Config.Server.CertFile, config.Config.Server.KeyFile) err = server.ListenAndServeTLS(config.Config.Server.TLS.CertFile, config.Config.Server.TLS.KeyFile)
} else { } else {
err = server.ListenAndServe() err = server.ListenAndServe()
} }
@ -45,20 +45,16 @@ func runServer(server *http.Server) {
func initServer() *http.Server { func initServer() *http.Server {
// API initialization // API initialization
nomadAPIClient, err := nomad.NewExecutorAPI( nomadAPIClient, err := nomad.NewExecutorAPI(&config.Config.Nomad)
config.Config.NomadAPIURL(),
config.Config.Nomad.Namespace,
config.Config.Nomad.Token,
)
if err != nil { if err != nil {
log.WithError(err).WithField("nomad url", config.Config.NomadAPIURL()).Fatal("Error parsing the nomad url") log.WithError(err).WithField("nomad config", config.Config.Nomad).Fatal("Error creating Nomad API client")
} }
runnerManager := runner.NewNomadRunnerManager(nomadAPIClient, context.Background()) runnerManager := runner.NewNomadRunnerManager(nomadAPIClient, context.Background())
environmentManager := environment.NewNomadEnvironmentManager(runnerManager, nomadAPIClient) environmentManager := environment.NewNomadEnvironmentManager(runnerManager, nomadAPIClient)
return &http.Server{ return &http.Server{
Addr: config.Config.PoseidonAPIURL().Host, Addr: config.Config.Server.URL().Host,
WriteTimeout: time.Second * 15, WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15, ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60, IdleTimeout: time.Second * 60,

View File

@ -6,12 +6,14 @@ server:
port: 7200 port: 7200
# If set, this token is required in the X-Poseidon-Token header for each route except /health # If set, this token is required in the X-Poseidon-Token header for each route except /health
token: SECRET token: SECRET
# If set, the API uses TLS for all incoming connections # Configuration of TLS between the web client and Poseidon.
tls: true tls:
# The path to the certificate file used for TLS # If set, the API uses TLS for all incoming connections.
certfile: ./poseidon.crt active: true
# The path to the key file used for TLS # The path to the certificate file used for TLS
keyfile: ./poseidon.key certfile: ./poseidon.crt
# The path to the key file used for TLS
keyfile: ./poseidon.key
# If true, an additional WebSocket connection will be opened to split stdout and stderr when executing interactively # If true, an additional WebSocket connection will be opened to split stdout and stderr when executing interactively
interactiveStderr: true interactiveStderr: true
@ -23,8 +25,16 @@ nomad:
port: 4646 port: 4646
# Authenticate requests to the Nomad server with this token # Authenticate requests to the Nomad server with this token
token: SECRET token: SECRET
# Specifies whether to use TLS when communicating with the Nomad server # Configuration of TLS between the Poseidon and Nomad.
tls: false tls:
# Specifies whether to use TLS when communicating with the Nomad server.
active: false
# The path to the certificate of the CA authority of the Nomad host.
cafile: ./ca.crt
# The path to the client certificate file used for TLS
certfile: ./poseidon.crt
# The path to the client key file used for TLS
keyfile: ./poseidon.key
# Nomad namespace to use. If unset, 'default' is used # Nomad namespace to use. If unset, 'default' is used
namespace: poseidon namespace: poseidon

View File

@ -93,7 +93,7 @@ func (r *RunnerController) execute(writer http.ResponseWriter, request *http.Req
} }
var scheme string var scheme string
if config.Config.Server.TLS { if config.Config.Server.TLS.Active {
scheme = "wss" scheme = "wss"
} else { } else {
scheme = "ws" scheme = "ws"

View File

@ -19,19 +19,26 @@ import (
var ( var (
Config = &configuration{ Config = &configuration{
Server: server{ Server: server{
Address: "127.0.0.1", Address: "127.0.0.1",
Port: 7200, Port: 7200,
Token: "", Token: "",
TLS: false, TLS: TLS{
CertFile: "", Active: false,
KeyFile: "", CertFile: "",
KeyFile: "",
},
InteractiveStderr: true, InteractiveStderr: true,
}, },
Nomad: nomad{ Nomad: Nomad{
Address: "127.0.0.1", Address: "127.0.0.1",
Port: 4646, Port: 4646,
Token: "", Token: "",
TLS: false, TLS: TLS{
Active: false,
CAFile: "",
CertFile: "",
KeyFile: "",
},
Namespace: "default", Namespace: "default",
}, },
Logger: logger{ Logger: logger{
@ -54,21 +61,37 @@ type server struct {
Address string Address string
Port int Port int
Token string Token string
TLS bool TLS TLS
CertFile string
KeyFile string
InteractiveStderr bool InteractiveStderr bool
} }
// nomad configures the used Nomad cluster. // URL returns the URL of the Poseidon webserver.
type nomad struct { func (s *server) URL() *url.URL {
return parseURL(s.Address, s.Port, s.TLS.Active)
}
// Nomad configures the used Nomad cluster.
type Nomad struct {
Address string Address string
Port int Port int
Token string Token string
TLS bool TLS TLS
Namespace string Namespace string
} }
// URL returns the URL for the configured Nomad cluster.
func (n *Nomad) URL() *url.URL {
return parseURL(n.Address, n.Port, n.TLS.Active)
}
// TLS configures TLS on a connection.
type TLS struct {
Active bool
CAFile string
CertFile string
KeyFile string
}
// logger configures the used logger. // logger configures the used logger.
type logger struct { type logger struct {
Level string Level string
@ -77,7 +100,7 @@ type logger struct {
// configuration contains the complete configuration of Poseidon. // configuration contains the complete configuration of Poseidon.
type configuration struct { type configuration struct {
Server server Server server
Nomad nomad Nomad Nomad
Logger logger Logger logger
} }
@ -96,16 +119,6 @@ func InitConfig() error {
return nil return nil
} }
// NomadAPIURL returns the URL for the configured Nomad cluster.
func (c *configuration) NomadAPIURL() *url.URL {
return parseURL(Config.Nomad.Address, Config.Nomad.Port, Config.Nomad.TLS)
}
// PoseidonAPIURL returns the URL of the Poseidon webserver.
func (c *configuration) PoseidonAPIURL() *url.URL {
return parseURL(Config.Server.Address, Config.Server.Port, false)
}
func parseURL(address string, port int, tlsEnabled bool) *url.URL { func parseURL(address string, port int, tlsEnabled bool) *url.URL {
scheme := "http" scheme := "http"
if tlsEnabled { if tlsEnabled {

View File

@ -13,9 +13,9 @@ import (
) )
var ( var (
getServerPort = func(c *configuration) interface{} { return c.Server.Port } getServerPort = func(c *configuration) interface{} { return c.Server.Port }
getNomadToken = func(c *configuration) interface{} { return c.Nomad.Token } getNomadToken = func(c *configuration) interface{} { return c.Nomad.Token }
getNomadTLS = func(c *configuration) interface{} { return c.Nomad.TLS } getNomadTLSActive = func(c *configuration) interface{} { return c.Nomad.TLS.Active }
) )
func newTestConfiguration() *configuration { func newTestConfiguration() *configuration {
@ -24,11 +24,13 @@ func newTestConfiguration() *configuration {
Address: "127.0.0.1", Address: "127.0.0.1",
Port: 3000, Port: 3000,
}, },
Nomad: nomad{ Nomad: Nomad{
Address: "127.0.0.2", Address: "127.0.0.2",
Port: 4646, Port: 4646,
Token: "SECRET", Token: "SECRET",
TLS: false, TLS: TLS{
Active: false,
},
}, },
Logger: logger{ Logger: logger{
Level: "INFO", Level: "INFO",
@ -87,8 +89,8 @@ func TestReadEnvironmentVariables(t *testing.T) {
{"SERVER_PORT", "4000", 4000, getServerPort}, {"SERVER_PORT", "4000", 4000, getServerPort},
{"SERVER_PORT", "hello", 3000, getServerPort}, {"SERVER_PORT", "hello", 3000, getServerPort},
{"NOMAD_TOKEN", "ACCESS", "ACCESS", getNomadToken}, {"NOMAD_TOKEN", "ACCESS", "ACCESS", getNomadToken},
{"NOMAD_TLS", "true", true, getNomadTLS}, {"NOMAD_TLS_ACTIVE", "true", true, getNomadTLSActive},
{"NOMAD_TLS", "hello", false, getNomadTLS}, {"NOMAD_TLS_ACTIVE", "hello", false, getNomadTLSActive},
} }
prefix := "POSEIDON_TEST" prefix := "POSEIDON_TEST"
for _, testCase := range environmentTests { for _, testCase := range environmentTests {
@ -131,8 +133,8 @@ func TestReadYamlConfigFile(t *testing.T) {
}{ }{
{[]byte("server:\n port: 5000\n"), 5000, getServerPort}, {[]byte("server:\n port: 5000\n"), 5000, getServerPort},
{[]byte("nomad:\n token: ACCESS\n"), "ACCESS", getNomadToken}, {[]byte("nomad:\n token: ACCESS\n"), "ACCESS", getNomadToken},
{[]byte("nomad:\n tls: true\n"), true, getNomadTLS}, {[]byte("nomad:\n tls:\n active: true\n"), true, getNomadTLSActive},
{[]byte(""), false, getNomadTLS}, {[]byte(""), false, getNomadTLSActive},
{[]byte("nomad:\n token:\n"), "SECRET", getNomadToken}, {[]byte("nomad:\n token:\n"), "SECRET", getNomadToken},
} }
for _, testCase := range yamlTests { for _, testCase := range yamlTests {
@ -197,12 +199,12 @@ func TestURLParsing(t *testing.T) {
func TestNomadAPIURL(t *testing.T) { func TestNomadAPIURL(t *testing.T) {
config := newTestConfiguration() config := newTestConfiguration()
assert.Equal(t, "http", config.NomadAPIURL().Scheme) assert.Equal(t, "http", config.Nomad.URL().Scheme)
assert.Equal(t, "127.0.0.2:4646", config.NomadAPIURL().Host) assert.Equal(t, "127.0.0.2:4646", config.Nomad.URL().Host)
} }
func TestPoseidonAPIURL(t *testing.T) { func TestPoseidonAPIURL(t *testing.T) {
config := newTestConfiguration() config := newTestConfiguration()
assert.Equal(t, "http", config.PoseidonAPIURL().Scheme) assert.Equal(t, "http", config.Server.URL().Scheme)
assert.Equal(t, "127.0.0.1:3000", config.PoseidonAPIURL().Host) assert.Equal(t, "127.0.0.1:3000", config.Server.URL().Host)
} }

View File

@ -5,8 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
nomadApi "github.com/hashicorp/nomad/api" nomadApi "github.com/hashicorp/nomad/api"
"gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config"
"io" "io"
"net/url"
) )
var ( var (
@ -16,7 +16,7 @@ var (
// apiQuerier provides access to the Nomad functionality. // apiQuerier provides access to the Nomad functionality.
type apiQuerier interface { type apiQuerier interface {
// init prepares an apiClient to be able to communicate to a provided Nomad API. // init prepares an apiClient to be able to communicate to a provided Nomad API.
init(nomadURL *url.URL, nomadNamespace, nomadToken string) (err error) init(nomadConfig *config.Nomad) (err error)
// LoadJobList loads the list of jobs from the Nomad API. // LoadJobList loads the list of jobs from the Nomad API.
LoadJobList() (list []*nomadApi.JobListStub, err error) LoadJobList() (list []*nomadApi.JobListStub, err error)
@ -61,17 +61,24 @@ type nomadAPIClient struct {
namespace string namespace string
} }
func (nc *nomadAPIClient) init(nomadURL *url.URL, nomadNamespace, nomadToken string) (err error) { func (nc *nomadAPIClient) init(nomadConfig *config.Nomad) (err error) {
nomadTLSConfig := &nomadApi.TLSConfig{}
if nomadConfig.TLS.Active {
nomadTLSConfig.CACert = nomadConfig.TLS.CAFile
nomadTLSConfig.ClientCert = nomadConfig.TLS.CertFile
nomadTLSConfig.ClientKey = nomadConfig.TLS.KeyFile
}
nc.client, err = nomadApi.NewClient(&nomadApi.Config{ nc.client, err = nomadApi.NewClient(&nomadApi.Config{
Address: nomadURL.String(), Address: nomadConfig.URL().String(),
TLSConfig: &nomadApi.TLSConfig{}, TLSConfig: nomadTLSConfig,
Namespace: nomadNamespace, Namespace: nomadConfig.Namespace,
SecretID: nomadToken, SecretID: nomadConfig.Token,
}) })
if err != nil { if err != nil {
return fmt.Errorf("error creating new Nomad client: %w", err) return fmt.Errorf("error creating new Nomad client: %w", err)
} }
nc.namespace = nomadNamespace nc.namespace = nomadConfig.Namespace
return nil return nil
} }

View File

@ -6,12 +6,11 @@ import (
context "context" context "context"
api "github.com/hashicorp/nomad/api" api "github.com/hashicorp/nomad/api"
config "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config"
io "io" io "io"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
url "net/url"
) )
// apiQuerierMock is an autogenerated mock type for the apiQuerier type // apiQuerierMock is an autogenerated mock type for the apiQuerier type
@ -202,13 +201,13 @@ func (_m *apiQuerierMock) allocation(jobID string) (*api.Allocation, error) {
return r0, r1 return r0, r1
} }
// init provides a mock function with given fields: nomadURL, nomadNamespace, nomadToken // init provides a mock function with given fields: nomadConfig
func (_m *apiQuerierMock) init(nomadURL *url.URL, nomadNamespace string, nomadToken string) error { func (_m *apiQuerierMock) init(nomadConfig *config.Nomad) error {
ret := _m.Called(nomadURL, nomadNamespace, nomadToken) ret := _m.Called(nomadConfig)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(*url.URL, string, string) error); ok { if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok {
r0 = rf(nomadURL, nomadNamespace, nomadToken) r0 = rf(nomadConfig)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }

View File

@ -6,12 +6,11 @@ import (
context "context" context "context"
api "github.com/hashicorp/nomad/api" api "github.com/hashicorp/nomad/api"
config "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config"
io "io" io "io"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
url "net/url"
) )
// ExecutorAPIMock is an autogenerated mock type for the ExecutorAPI type // ExecutorAPIMock is an autogenerated mock type for the ExecutorAPI type
@ -394,13 +393,13 @@ func (_m *ExecutorAPIMock) allocation(jobID string) (*api.Allocation, error) {
return r0, r1 return r0, r1
} }
// init provides a mock function with given fields: nomadURL, nomadNamespace, nomadToken // init provides a mock function with given fields: nomadConfig
func (_m *ExecutorAPIMock) init(nomadURL *url.URL, nomadNamespace string, nomadToken string) error { func (_m *ExecutorAPIMock) init(nomadConfig *config.Nomad) error {
ret := _m.Called(nomadURL, nomadNamespace, nomadToken) ret := _m.Called(nomadConfig)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(*url.URL, string, string) error); ok { if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok {
r0 = rf(nomadURL, nomadNamespace, nomadToken) r0 = rf(nomadConfig)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }

View File

@ -10,7 +10,6 @@ import (
"gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/logging" "gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/logging"
"gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/nullio" "gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/nullio"
"io" "io"
"net/url"
"strconv" "strconv"
"time" "time"
) )
@ -81,15 +80,15 @@ type APIClient struct {
// NewExecutorAPI creates a new api client. // NewExecutorAPI creates a new api client.
// One client is usually sufficient for the complete runtime of the API. // One client is usually sufficient for the complete runtime of the API.
func NewExecutorAPI(nomadURL *url.URL, nomadNamespace, nomadToken string) (ExecutorAPI, error) { func NewExecutorAPI(nomadConfig *config.Nomad) (ExecutorAPI, error) {
client := &APIClient{apiQuerier: &nomadAPIClient{}} client := &APIClient{apiQuerier: &nomadAPIClient{}}
err := client.init(nomadURL, nomadNamespace, nomadToken) err := client.init(nomadConfig)
return client, err return client, err
} }
// init prepares an apiClient to be able to communicate to a provided Nomad API. // init prepares an apiClient to be able to communicate to a provided Nomad API.
func (a *APIClient) init(nomadURL *url.URL, nomadNamespace, nomadToken string) error { func (a *APIClient) init(nomadConfig *config.Nomad) error {
if err := a.apiQuerier.init(nomadURL, nomadNamespace, nomadToken); err != nil { if err := a.apiQuerier.init(nomadConfig); err != nil {
return fmt.Errorf("error initializing API querier: %w", err) return fmt.Errorf("error initializing API querier: %w", err)
} }
return nil return nil

View File

@ -131,28 +131,38 @@ var (
const TestNamespace = "unit-tests" const TestNamespace = "unit-tests"
const TestNomadToken = "n0m4d-t0k3n" const TestNomadToken = "n0m4d-t0k3n"
const TestDefaultAddress = "127.0.0.1"
func NomadTestConfig(address string) *config.Nomad {
return &config.Nomad{
Address: address,
Port: 4646,
Token: TestNomadToken,
TLS: config.TLS{
Active: false,
},
Namespace: TestNamespace,
}
}
func TestApiClient_init(t *testing.T) { func TestApiClient_init(t *testing.T) {
client := &APIClient{apiQuerier: &nomadAPIClient{}} client := &APIClient{apiQuerier: &nomadAPIClient{}}
err := client.init(&TestURL, TestNamespace, TestNomadToken) err := client.init(NomadTestConfig(TestDefaultAddress))
require.Nil(t, err) require.Nil(t, err)
} }
func TestApiClientCanNotBeInitializedWithInvalidUrl(t *testing.T) { func TestApiClientCanNotBeInitializedWithInvalidUrl(t *testing.T) {
client := &APIClient{apiQuerier: &nomadAPIClient{}} client := &APIClient{apiQuerier: &nomadAPIClient{}}
err := client.init(&url.URL{ err := client.init(NomadTestConfig("http://" + TestDefaultAddress))
Scheme: "http",
Host: "http://127.0.0.1:4646",
}, TestNamespace, TestNomadToken)
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func TestNewExecutorApiCanBeCreatedWithoutError(t *testing.T) { func TestNewExecutorApiCanBeCreatedWithoutError(t *testing.T) {
expectedClient := &APIClient{apiQuerier: &nomadAPIClient{}} expectedClient := &APIClient{apiQuerier: &nomadAPIClient{}}
err := expectedClient.init(&TestURL, TestNamespace, TestNomadToken) err := expectedClient.init(NomadTestConfig(TestDefaultAddress))
require.Nil(t, err) require.Nil(t, err)
_, err = NewExecutorAPI(&TestURL, TestNamespace, TestNomadToken) _, err = NewExecutorAPI(NomadTestConfig(TestDefaultAddress))
require.Nil(t, err) require.Nil(t, err)
} }

View File

@ -51,7 +51,7 @@ func TestMain(m *testing.M) {
} }
nomadNamespace = config.Config.Nomad.Namespace nomadNamespace = config.Config.Nomad.Namespace
nomadClient, err = nomadApi.NewClient(&nomadApi.Config{ nomadClient, err = nomadApi.NewClient(&nomadApi.Config{
Address: config.Config.NomadAPIURL().String(), Address: config.Config.Nomad.URL().String(),
TLSConfig: &nomadApi.TLSConfig{}, TLSConfig: &nomadApi.TLSConfig{},
Namespace: nomadNamespace, Namespace: nomadNamespace,
}) })

View File

@ -24,7 +24,7 @@ import (
// BuildURL joins multiple route paths. // BuildURL joins multiple route paths.
func BuildURL(parts ...string) string { func BuildURL(parts ...string) string {
url := config.Config.PoseidonAPIURL().String() url := config.Config.Server.URL().String()
for _, part := range parts { for _, part := range parts {
if !strings.HasPrefix(part, "/") { if !strings.HasPrefix(part, "/") {
url += "/" url += "/"