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 }