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 }