github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocrunner/taskrunner/connect_native_hook.go (about)

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	hclog "github.com/hashicorp/go-hclog"
    11  	"github.com/hashicorp/nomad/client/allocdir"
    12  	ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
    13  	"github.com/hashicorp/nomad/nomad/structs"
    14  	"github.com/hashicorp/nomad/nomad/structs/config"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  const (
    19  	connectNativeHookName = "connect_native"
    20  )
    21  
    22  type connectNativeHookConfig struct {
    23  	consulShareTLS bool
    24  	consul         consulTransportConfig
    25  	alloc          *structs.Allocation
    26  	logger         hclog.Logger
    27  }
    28  
    29  func newConnectNativeHookConfig(alloc *structs.Allocation, consul *config.ConsulConfig, logger hclog.Logger) *connectNativeHookConfig {
    30  	return &connectNativeHookConfig{
    31  		alloc:          alloc,
    32  		logger:         logger,
    33  		consulShareTLS: consul.ShareSSL == nil || *consul.ShareSSL, // default enabled
    34  		consul:         newConsulTransportConfig(consul),
    35  	}
    36  }
    37  
    38  // connectNativeHook manages additional automagic configuration for a connect
    39  // native task.
    40  //
    41  // If nomad client is configured to talk to Consul using TLS (or other special
    42  // auth), the native task will inherit that configuration EXCEPT for the consul
    43  // token.
    44  //
    45  // If consul is configured with ACLs enabled, a Service Identity token will be
    46  // generated on behalf of the native service and supplied to the task.
    47  //
    48  // If the alloc is configured with bridge networking enabled, the standard
    49  // CONSUL_HTTP_ADDR environment variable is defaulted to the unix socket created
    50  // for the alloc by the consul_grpc_sock_hook alloc runner hook.
    51  type connectNativeHook struct {
    52  	// alloc is the allocation with the connect native task being run
    53  	alloc *structs.Allocation
    54  
    55  	// consulShareTLS is used to toggle whether the TLS configuration of the
    56  	// Nomad Client may be shared with Connect Native applications.
    57  	consulShareTLS bool
    58  
    59  	// consulConfig is used to enable the connect native enabled task to
    60  	// communicate with consul directly, as is necessary for the task to request
    61  	// its connect mTLS certificates.
    62  	consulConfig consulTransportConfig
    63  
    64  	// logger is used to log things
    65  	logger hclog.Logger
    66  }
    67  
    68  func newConnectNativeHook(c *connectNativeHookConfig) *connectNativeHook {
    69  	return &connectNativeHook{
    70  		alloc:          c.alloc,
    71  		consulShareTLS: c.consulShareTLS,
    72  		consulConfig:   c.consul,
    73  		logger:         c.logger.Named(connectNativeHookName),
    74  	}
    75  }
    76  
    77  func (connectNativeHook) Name() string {
    78  	return connectNativeHookName
    79  }
    80  
    81  // merge b into a, overwriting on conflicts
    82  func merge(a, b map[string]string) {
    83  	for k, v := range b {
    84  		a[k] = v
    85  	}
    86  }
    87  
    88  func (h *connectNativeHook) Prestart(
    89  	ctx context.Context,
    90  	request *ifs.TaskPrestartRequest,
    91  	response *ifs.TaskPrestartResponse) error {
    92  
    93  	if !request.Task.Kind.IsConnectNative() {
    94  		response.Done = true
    95  		return nil
    96  	}
    97  
    98  	environment := make(map[string]string)
    99  
   100  	if h.consulShareTLS {
   101  		// copy TLS certificates
   102  		if err := h.copyCertificates(h.consulConfig, request.TaskDir.SecretsDir); err != nil {
   103  			h.logger.Error("failed to copy Consul TLS certificates", "error", err)
   104  			return err
   105  		}
   106  
   107  		// set environment variables for communicating with Consul agent, but
   108  		// only if those environment variables are not already set
   109  		merge(environment, h.tlsEnv(request.TaskEnv.EnvMap))
   110  	}
   111  
   112  	if err := h.maybeSetSITokenEnv(request.TaskDir.SecretsDir, request.Task.Name, environment); err != nil {
   113  		h.logger.Error("failed to load Consul Service Identity Token", "error", err, "task", request.Task.Name)
   114  		return err
   115  	}
   116  
   117  	merge(environment, h.bridgeEnv(request.TaskEnv.EnvMap))
   118  
   119  	// tls/acl setup for native task done
   120  	response.Done = true
   121  	response.Env = environment
   122  	return nil
   123  }
   124  
   125  const (
   126  	secretCAFilename       = "consul_ca_file.pem"
   127  	secretCertfileFilename = "consul_cert_file.pem"
   128  	secretKeyfileFilename  = "consul_key_file.pem"
   129  )
   130  
   131  func (h *connectNativeHook) copyCertificates(consulConfig consulTransportConfig, dir string) error {
   132  	if err := h.copyCertificate(consulConfig.CAFile, dir, secretCAFilename); err != nil {
   133  		return err
   134  	}
   135  	if err := h.copyCertificate(consulConfig.CertFile, dir, secretCertfileFilename); err != nil {
   136  		return err
   137  	}
   138  	if err := h.copyCertificate(consulConfig.KeyFile, dir, secretKeyfileFilename); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  func (connectNativeHook) copyCertificate(source, dir, name string) error {
   145  	if source == "" {
   146  		return nil
   147  	}
   148  
   149  	original, err := os.Open(source)
   150  	if err != nil {
   151  		return errors.Wrap(err, "failed to open consul TLS certificate")
   152  	}
   153  	defer original.Close()
   154  
   155  	destination := filepath.Join(dir, name)
   156  	fd, err := os.Create(destination)
   157  	if err != nil {
   158  		return errors.Wrapf(err, "failed to create secrets/%s", name)
   159  	}
   160  	defer fd.Close()
   161  
   162  	if _, err := io.Copy(fd, original); err != nil {
   163  		return errors.Wrapf(err, "failed to copy certificate secrets/%s", name)
   164  	}
   165  
   166  	if err := fd.Sync(); err != nil {
   167  		return errors.Wrapf(err, "failed to write secrets/%s", name)
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // tlsEnv creates a set of additional of environment variables to be used when launching
   174  // the connect native task. This will enable the task to communicate with Consul
   175  // if Consul has transport security turned on.
   176  //
   177  // We do NOT set CONSUL_HTTP_TOKEN from the nomad agent's consul config, as that
   178  // is a separate security concern addressed by the service identity hook.
   179  func (h *connectNativeHook) tlsEnv(env map[string]string) map[string]string {
   180  	m := make(map[string]string)
   181  
   182  	if _, exists := env["CONSUL_CACERT"]; !exists && h.consulConfig.CAFile != "" {
   183  		m["CONSUL_CACERT"] = filepath.Join("/secrets", secretCAFilename)
   184  	}
   185  
   186  	if _, exists := env["CONSUL_CLIENT_CERT"]; !exists && h.consulConfig.CertFile != "" {
   187  		m["CONSUL_CLIENT_CERT"] = filepath.Join("/secrets", secretCertfileFilename)
   188  	}
   189  
   190  	if _, exists := env["CONSUL_CLIENT_KEY"]; !exists && h.consulConfig.KeyFile != "" {
   191  		m["CONSUL_CLIENT_KEY"] = filepath.Join("/secrets", secretKeyfileFilename)
   192  	}
   193  
   194  	if _, exists := env["CONSUL_HTTP_SSL"]; !exists {
   195  		if v := h.consulConfig.SSL; v != "" {
   196  			m["CONSUL_HTTP_SSL"] = v
   197  		}
   198  	}
   199  
   200  	if _, exists := env["CONSUL_HTTP_SSL_VERIFY"]; !exists {
   201  		if v := h.consulConfig.VerifySSL; v != "" {
   202  			m["CONSUL_HTTP_SSL_VERIFY"] = v
   203  		}
   204  	}
   205  
   206  	return m
   207  }
   208  
   209  // bridgeEnv creates a set of additional environment variables to be used when launching
   210  // the connect native task. This will enable the task to communicate with Consul
   211  // if the task is running inside an alloc's network namespace (i.e. bridge mode).
   212  //
   213  // Sets CONSUL_HTTP_ADDR if not already set.
   214  func (h *connectNativeHook) bridgeEnv(env map[string]string) map[string]string {
   215  	if h.alloc.AllocatedResources.Shared.Networks[0].Mode != "bridge" {
   216  		return nil
   217  	}
   218  
   219  	if _, exists := env["CONSUL_HTTP_ADDR"]; !exists {
   220  		return map[string]string{
   221  			"CONSUL_HTTP_ADDR": "unix:///" + allocdir.AllocHTTPSocket,
   222  		}
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  // maybeSetSITokenEnv will set the CONSUL_HTTP_TOKEN environment variable in
   229  // the given env map, if the token is found to exist in the task's secrets
   230  // directory AND the CONSUL_HTTP_TOKEN environment variable is not already set.
   231  //
   232  // Following the pattern of the envoy_bootstrap_hook, the Consul Service Identity
   233  // ACL Token is generated prior to this hook, if Consul ACLs are enabled. This is
   234  // done in the sids_hook, which places the token at secrets/si_token in the task
   235  // workspace. The content of that file is the SI token specific to this task
   236  // instance.
   237  func (h *connectNativeHook) maybeSetSITokenEnv(dir, task string, env map[string]string) error {
   238  	if _, exists := env["CONSUL_HTTP_TOKEN"]; exists {
   239  		// Consul token was already set - typically by using the Vault integration
   240  		// and a template stanza to set the environment. Ignore the SI token as
   241  		// the configured token takes precedence.
   242  		return nil
   243  	}
   244  
   245  	token, err := ioutil.ReadFile(filepath.Join(dir, sidsTokenFile))
   246  	if err != nil {
   247  		if !os.IsNotExist(err) {
   248  			return errors.Wrapf(err, "failed to load SI token for native task %s", task)
   249  		}
   250  		h.logger.Trace("no SI token to load for native task", "task", task)
   251  		return nil // token file DNE; acls not enabled
   252  	}
   253  	h.logger.Trace("recovered pre-existing SI token for native task", "task", task)
   254  	env["CONSUL_HTTP_TOKEN"] = string(token)
   255  	return nil
   256  }