5aa657c6bd
This PR makes it so that Nomad will automatically set the CONSUL_TLS_SERVER_NAME environment variable for Connect native tasks running in bridge networking mode where Consul has TLS enabled. Because of the use of a unix domain socket for communicating with Consul when in bridge networking mode, the server name is a file name instead of something compatible with the mTLS certificate Consul will authenticate against. "localhost" is by default a compatible name, so Nomad will set the environment variable to that. Fixes #10804
285 lines
8.9 KiB
Go
285 lines
8.9 KiB
Go
package taskrunner
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/nomad/client/allocdir"
|
|
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
connectNativeHookName = "connect_native"
|
|
)
|
|
|
|
type connectNativeHookConfig struct {
|
|
consulShareTLS bool
|
|
consul consulTransportConfig
|
|
alloc *structs.Allocation
|
|
logger hclog.Logger
|
|
}
|
|
|
|
func newConnectNativeHookConfig(alloc *structs.Allocation, consul *config.ConsulConfig, logger hclog.Logger) *connectNativeHookConfig {
|
|
return &connectNativeHookConfig{
|
|
alloc: alloc,
|
|
logger: logger,
|
|
consulShareTLS: consul.ShareSSL == nil || *consul.ShareSSL, // default enabled
|
|
consul: newConsulTransportConfig(consul),
|
|
}
|
|
}
|
|
|
|
// connectNativeHook manages additional automagic configuration for a connect
|
|
// native task.
|
|
//
|
|
// If nomad client is configured to talk to Consul using TLS (or other special
|
|
// auth), the native task will inherit that configuration EXCEPT for the consul
|
|
// token.
|
|
//
|
|
// If consul is configured with ACLs enabled, a Service Identity token will be
|
|
// generated on behalf of the native service and supplied to the task.
|
|
//
|
|
// If the alloc is configured with bridge networking enabled, the standard
|
|
// CONSUL_HTTP_ADDR environment variable is defaulted to the unix socket created
|
|
// for the alloc by the consul_grpc_sock_hook alloc runner hook.
|
|
type connectNativeHook struct {
|
|
// alloc is the allocation with the connect native task being run
|
|
alloc *structs.Allocation
|
|
|
|
// consulShareTLS is used to toggle whether the TLS configuration of the
|
|
// Nomad Client may be shared with Connect Native applications.
|
|
consulShareTLS bool
|
|
|
|
// consulConfig is used to enable the connect native enabled task to
|
|
// communicate with consul directly, as is necessary for the task to request
|
|
// its connect mTLS certificates.
|
|
consulConfig consulTransportConfig
|
|
|
|
// logger is used to log things
|
|
logger hclog.Logger
|
|
}
|
|
|
|
func newConnectNativeHook(c *connectNativeHookConfig) *connectNativeHook {
|
|
return &connectNativeHook{
|
|
alloc: c.alloc,
|
|
consulShareTLS: c.consulShareTLS,
|
|
consulConfig: c.consul,
|
|
logger: c.logger.Named(connectNativeHookName),
|
|
}
|
|
}
|
|
|
|
func (connectNativeHook) Name() string {
|
|
return connectNativeHookName
|
|
}
|
|
|
|
// merge b into a, overwriting on conflicts
|
|
func merge(a, b map[string]string) {
|
|
for k, v := range b {
|
|
a[k] = v
|
|
}
|
|
}
|
|
|
|
func (h *connectNativeHook) Prestart(
|
|
ctx context.Context,
|
|
request *ifs.TaskPrestartRequest,
|
|
response *ifs.TaskPrestartResponse) error {
|
|
|
|
if !request.Task.Kind.IsConnectNative() {
|
|
response.Done = true
|
|
return nil
|
|
}
|
|
|
|
environment := make(map[string]string)
|
|
|
|
if h.consulShareTLS {
|
|
// copy TLS certificates
|
|
if err := h.copyCertificates(h.consulConfig, request.TaskDir.SecretsDir); err != nil {
|
|
h.logger.Error("failed to copy Consul TLS certificates", "error", err)
|
|
return err
|
|
}
|
|
|
|
// set environment variables for communicating with Consul agent, but
|
|
// only if those environment variables are not already set
|
|
merge(environment, h.tlsEnv(request.TaskEnv.EnvMap))
|
|
}
|
|
|
|
if err := h.maybeSetSITokenEnv(request.TaskDir.SecretsDir, request.Task.Name, environment); err != nil {
|
|
h.logger.Error("failed to load Consul Service Identity Token", "error", err, "task", request.Task.Name)
|
|
return err
|
|
}
|
|
|
|
merge(environment, h.bridgeEnv(request.TaskEnv.EnvMap))
|
|
merge(environment, h.hostEnv(request.TaskEnv.EnvMap))
|
|
|
|
// tls/acl setup for native task done
|
|
response.Done = true
|
|
response.Env = environment
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
secretCAFilename = "consul_ca_file.pem"
|
|
secretCertfileFilename = "consul_cert_file.pem"
|
|
secretKeyfileFilename = "consul_key_file.pem"
|
|
)
|
|
|
|
func (h *connectNativeHook) copyCertificates(consulConfig consulTransportConfig, dir string) error {
|
|
if err := h.copyCertificate(consulConfig.CAFile, dir, secretCAFilename); err != nil {
|
|
return err
|
|
}
|
|
if err := h.copyCertificate(consulConfig.CertFile, dir, secretCertfileFilename); err != nil {
|
|
return err
|
|
}
|
|
if err := h.copyCertificate(consulConfig.KeyFile, dir, secretKeyfileFilename); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (connectNativeHook) copyCertificate(source, dir, name string) error {
|
|
if source == "" {
|
|
return nil
|
|
}
|
|
|
|
original, err := os.Open(source)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to open consul TLS certificate")
|
|
}
|
|
defer original.Close()
|
|
|
|
destination := filepath.Join(dir, name)
|
|
fd, err := os.Create(destination)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to create secrets/%s", name)
|
|
}
|
|
defer fd.Close()
|
|
|
|
if _, err := io.Copy(fd, original); err != nil {
|
|
return errors.Wrapf(err, "failed to copy certificate secrets/%s", name)
|
|
}
|
|
|
|
if err := fd.Sync(); err != nil {
|
|
return errors.Wrapf(err, "failed to write secrets/%s", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// tlsEnv creates a set of additional of environment variables to be used when launching
|
|
// the connect native task. This will enable the task to communicate with Consul
|
|
// if Consul has transport security turned on.
|
|
//
|
|
// We do NOT set CONSUL_HTTP_TOKEN from the nomad agent's consul config, as that
|
|
// is a separate security concern addressed by the service identity hook.
|
|
func (h *connectNativeHook) tlsEnv(env map[string]string) map[string]string {
|
|
m := make(map[string]string)
|
|
|
|
if _, exists := env["CONSUL_CACERT"]; !exists && h.consulConfig.CAFile != "" {
|
|
m["CONSUL_CACERT"] = filepath.Join("/secrets", secretCAFilename)
|
|
}
|
|
|
|
if _, exists := env["CONSUL_CLIENT_CERT"]; !exists && h.consulConfig.CertFile != "" {
|
|
m["CONSUL_CLIENT_CERT"] = filepath.Join("/secrets", secretCertfileFilename)
|
|
}
|
|
|
|
if _, exists := env["CONSUL_CLIENT_KEY"]; !exists && h.consulConfig.KeyFile != "" {
|
|
m["CONSUL_CLIENT_KEY"] = filepath.Join("/secrets", secretKeyfileFilename)
|
|
}
|
|
|
|
if _, exists := env["CONSUL_HTTP_SSL"]; !exists {
|
|
if v := h.consulConfig.SSL; v != "" {
|
|
m["CONSUL_HTTP_SSL"] = v
|
|
}
|
|
}
|
|
|
|
if _, exists := env["CONSUL_HTTP_SSL_VERIFY"]; !exists {
|
|
if v := h.consulConfig.VerifySSL; v != "" {
|
|
m["CONSUL_HTTP_SSL_VERIFY"] = v
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// bridgeEnv creates a set of additional environment variables to be used when launching
|
|
// the connect native task. This will enable the task to communicate with Consul
|
|
// if the task is running inside an alloc's network namespace (i.e. bridge mode).
|
|
//
|
|
// Sets CONSUL_HTTP_ADDR if not already set.
|
|
// Sets CONSUL_TLS_SERVER_NAME if not already set, and consul tls is enabled.
|
|
func (h *connectNativeHook) bridgeEnv(env map[string]string) map[string]string {
|
|
|
|
if h.alloc.AllocatedResources.Shared.Networks[0].Mode != "bridge" {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]string)
|
|
|
|
if _, exists := env["CONSUL_HTTP_ADDR"]; !exists {
|
|
result["CONSUL_HTTP_ADDR"] = "unix:///" + allocdir.AllocHTTPSocket
|
|
}
|
|
|
|
if _, exists := env["CONSUL_TLS_SERVER_NAME"]; !exists {
|
|
if v := h.consulConfig.SSL; v != "" {
|
|
result["CONSUL_TLS_SERVER_NAME"] = "localhost"
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// hostEnv creates a set of additional environment variables to be used when launching
|
|
// the connect native task. This will enable the task to communicate with Consul
|
|
// if the task is running in host network mode.
|
|
//
|
|
// Sets CONSUL_HTTP_ADDR if not already set.
|
|
func (h *connectNativeHook) hostEnv(env map[string]string) map[string]string {
|
|
if h.alloc.AllocatedResources.Shared.Networks[0].Mode != "host" {
|
|
return nil
|
|
}
|
|
|
|
if _, exists := env["CONSUL_HTTP_ADDR"]; !exists {
|
|
return map[string]string{
|
|
"CONSUL_HTTP_ADDR": h.consulConfig.HTTPAddr,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// maybeSetSITokenEnv will set the CONSUL_HTTP_TOKEN environment variable in
|
|
// the given env map, if the token is found to exist in the task's secrets
|
|
// directory AND the CONSUL_HTTP_TOKEN environment variable is not already set.
|
|
//
|
|
// Following the pattern of the envoy_bootstrap_hook, the Consul Service Identity
|
|
// ACL Token is generated prior to this hook, if Consul ACLs are enabled. This is
|
|
// done in the sids_hook, which places the token at secrets/si_token in the task
|
|
// workspace. The content of that file is the SI token specific to this task
|
|
// instance.
|
|
func (h *connectNativeHook) maybeSetSITokenEnv(dir, task string, env map[string]string) error {
|
|
if _, exists := env["CONSUL_HTTP_TOKEN"]; exists {
|
|
// Consul token was already set - typically by using the Vault integration
|
|
// and a template stanza to set the environment. Ignore the SI token as
|
|
// the configured token takes precedence.
|
|
return nil
|
|
}
|
|
|
|
token, err := ioutil.ReadFile(filepath.Join(dir, sidsTokenFile))
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "failed to load SI token for native task %s", task)
|
|
}
|
|
h.logger.Trace("no SI token to load for native task", "task", task)
|
|
return nil // token file DNE; acls not enabled
|
|
}
|
|
h.logger.Trace("recovered pre-existing SI token for native task", "task", task)
|
|
env["CONSUL_HTTP_TOKEN"] = string(token)
|
|
return nil
|
|
}
|