github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/connect_native_hook.go (about)

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/hashicorp/go-hclog"
    12  	"github.com/hashicorp/nomad/client/allocdir"
    13  	ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/hashicorp/nomad/nomad/structs/config"
    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  	merge(environment, h.hostEnv(request.TaskEnv.EnvMap))
   119  
   120  	// tls/acl setup for native task done
   121  	response.Done = true
   122  	response.Env = environment
   123  	return nil
   124  }
   125  
   126  const (
   127  	secretCAFilename       = "consul_ca_file.pem"
   128  	secretCertfileFilename = "consul_cert_file.pem"
   129  	secretKeyfileFilename  = "consul_key_file.pem"
   130  )
   131  
   132  func (h *connectNativeHook) copyCertificates(consulConfig consulTransportConfig, dir string) error {
   133  	if err := h.copyCertificate(consulConfig.CAFile, dir, secretCAFilename); err != nil {
   134  		return err
   135  	}
   136  	if err := h.copyCertificate(consulConfig.CertFile, dir, secretCertfileFilename); err != nil {
   137  		return err
   138  	}
   139  	if err := h.copyCertificate(consulConfig.KeyFile, dir, secretKeyfileFilename); err != nil {
   140  		return err
   141  	}
   142  	return nil
   143  }
   144  
   145  func (connectNativeHook) copyCertificate(source, dir, name string) error {
   146  	if source == "" {
   147  		return nil
   148  	}
   149  
   150  	original, err := os.Open(source)
   151  	if err != nil {
   152  		return fmt.Errorf("failed to open consul TLS certificate: %w", err)
   153  	}
   154  	defer original.Close()
   155  
   156  	destination := filepath.Join(dir, name)
   157  	fd, err := os.Create(destination)
   158  	if err != nil {
   159  		return fmt.Errorf("failed to create secrets/%s: %w", name, err)
   160  	}
   161  	defer fd.Close()
   162  
   163  	if _, err := io.Copy(fd, original); err != nil {
   164  		return fmt.Errorf("failed to copy certificate secrets/%s: %w", name, err)
   165  	}
   166  
   167  	if err := fd.Sync(); err != nil {
   168  		return fmt.Errorf("failed to write secrets/%s: %w", name, err)
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  // tlsEnv creates a set of additional of environment variables to be used when launching
   175  // the connect native task. This will enable the task to communicate with Consul
   176  // if Consul has transport security turned on.
   177  //
   178  // We do NOT set CONSUL_HTTP_TOKEN from the nomad agent's consul config, as that
   179  // is a separate security concern addressed by the service identity hook.
   180  func (h *connectNativeHook) tlsEnv(env map[string]string) map[string]string {
   181  	m := make(map[string]string)
   182  
   183  	if _, exists := env["CONSUL_CACERT"]; !exists && h.consulConfig.CAFile != "" {
   184  		m["CONSUL_CACERT"] = filepath.Join("/secrets", secretCAFilename)
   185  	}
   186  
   187  	if _, exists := env["CONSUL_CLIENT_CERT"]; !exists && h.consulConfig.CertFile != "" {
   188  		m["CONSUL_CLIENT_CERT"] = filepath.Join("/secrets", secretCertfileFilename)
   189  	}
   190  
   191  	if _, exists := env["CONSUL_CLIENT_KEY"]; !exists && h.consulConfig.KeyFile != "" {
   192  		m["CONSUL_CLIENT_KEY"] = filepath.Join("/secrets", secretKeyfileFilename)
   193  	}
   194  
   195  	if _, exists := env["CONSUL_HTTP_SSL"]; !exists {
   196  		if v := h.consulConfig.SSL; v != "" {
   197  			m["CONSUL_HTTP_SSL"] = v
   198  		}
   199  	}
   200  
   201  	if _, exists := env["CONSUL_HTTP_SSL_VERIFY"]; !exists {
   202  		if v := h.consulConfig.VerifySSL; v != "" {
   203  			m["CONSUL_HTTP_SSL_VERIFY"] = v
   204  		}
   205  	}
   206  
   207  	return m
   208  }
   209  
   210  // bridgeEnv creates a set of additional environment variables to be used when launching
   211  // the connect native task. This will enable the task to communicate with Consul
   212  // if the task is running inside an alloc's network namespace (i.e. bridge mode).
   213  //
   214  // Sets CONSUL_HTTP_ADDR if not already set.
   215  // Sets CONSUL_TLS_SERVER_NAME if not already set, and consul tls is enabled.
   216  func (h *connectNativeHook) bridgeEnv(env map[string]string) map[string]string {
   217  
   218  	if h.alloc.AllocatedResources.Shared.Networks[0].Mode != "bridge" {
   219  		return nil
   220  	}
   221  
   222  	result := make(map[string]string)
   223  
   224  	if _, exists := env["CONSUL_HTTP_ADDR"]; !exists {
   225  		result["CONSUL_HTTP_ADDR"] = "unix:///" + allocdir.AllocHTTPSocket
   226  	}
   227  
   228  	if _, exists := env["CONSUL_TLS_SERVER_NAME"]; !exists {
   229  		if v := h.consulConfig.SSL; v != "" {
   230  			result["CONSUL_TLS_SERVER_NAME"] = "localhost"
   231  		}
   232  	}
   233  
   234  	return result
   235  }
   236  
   237  // hostEnv creates a set of additional environment variables to be used when launching
   238  // the connect native task. This will enable the task to communicate with Consul
   239  // if the task is running in host network mode.
   240  //
   241  // Sets CONSUL_HTTP_ADDR if not already set.
   242  func (h *connectNativeHook) hostEnv(env map[string]string) map[string]string {
   243  	if h.alloc.AllocatedResources.Shared.Networks[0].Mode != "host" {
   244  		return nil
   245  	}
   246  
   247  	if _, exists := env["CONSUL_HTTP_ADDR"]; !exists {
   248  		return map[string]string{
   249  			"CONSUL_HTTP_ADDR": h.consulConfig.HTTPAddr,
   250  		}
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  // maybeSetSITokenEnv will set the CONSUL_HTTP_TOKEN environment variable in
   257  // the given env map, if the token is found to exist in the task's secrets
   258  // directory AND the CONSUL_HTTP_TOKEN environment variable is not already set.
   259  //
   260  // Following the pattern of the envoy_bootstrap_hook, the Consul Service Identity
   261  // ACL Token is generated prior to this hook, if Consul ACLs are enabled. This is
   262  // done in the sids_hook, which places the token at secrets/si_token in the task
   263  // workspace. The content of that file is the SI token specific to this task
   264  // instance.
   265  func (h *connectNativeHook) maybeSetSITokenEnv(dir, task string, env map[string]string) error {
   266  	if _, exists := env["CONSUL_HTTP_TOKEN"]; exists {
   267  		// Consul token was already set - typically by using the Vault integration
   268  		// and a template stanza to set the environment. Ignore the SI token as
   269  		// the configured token takes precedence.
   270  		return nil
   271  	}
   272  
   273  	token, err := ioutil.ReadFile(filepath.Join(dir, sidsTokenFile))
   274  	if err != nil {
   275  		if !os.IsNotExist(err) {
   276  			return fmt.Errorf("failed to load SI token for native task %s: %w", task, err)
   277  		}
   278  		h.logger.Trace("no SI token to load for native task", "task", task)
   279  		return nil // token file DNE; acls not enabled
   280  	}
   281  	h.logger.Trace("recovered pre-existing SI token for native task", "task", task)
   282  	env["CONSUL_HTTP_TOKEN"] = string(token)
   283  	return nil
   284  }