vault/command/agent/template/template.go
Vault Automation e018b0c436
Implement PKIPublicCA config parsing and handling (#12363) (#13368)
* Update config.go

* added validation and parsing

* tests

* move pki external config structs and validation into separate file

* update copywrite

* update configuration

* updates

* Moved tests to pki_external_config.go, comments, refactoring

* refactor

* add tests

* linter fix

* Consolidate to table tests

* consolidate to table tests

* remove APIVersion from PKIExternalCA

* added comments for explaining each struct

* Added ParsePKIExternalCA Test

* Update tests

* Added remaining constraints

* Added destination.template field

* changes

* Added validateListenerAddr

* refactor

* more comments

* changes

* Check for duplicates across blocks

* Make RSA bits a required field

* moved template to the top level

* added comment for test explanation

* move template to the top level

* Move pki config into pkiexternalca directory

* fix linting error

* move pkiconfig back into config folder

* fix failing unit tests

* added comments

* update to preserve order of templatePKIExternalCARefs

* Added comment descriptions for each struct member

* update to include warning

* bring in warning logger from upstream into the pki config parser

* Set default umask to 077

* added comments to each field in agent config

* execute tests in parallel

* combine tests into Validate

* Use assertion error func for tests

* assert error strings

* Removed warning for now

* removed normalization on values during validation

* added tests to ensure that user values are not overridden

* remove testparse

* Update command/agent/config/config.go



* change improvement to feature in changelog

* updated to add line number in error

* Added _ent suffix to files

* Implement CA manager for ACME-based workflows (#12827)

* Implement CA manager for ACME-based workflows

* refactor tests into table tests

* update with suggestions

* format

* fix challenge cleanup

* make fmt

* update with suggestions

* add _ent + build flags

* Add a runtime component for pkiexternalca (#12838)

* Implement CA manager for ACME-based workflows

* Add a runtime component for pkiexternalca

* make fmt

* refactor tests into table tests

* update with suggestions

* format

* fix challenge cleanup

* make fmt

* update with suggestions

* update with suggestions

* add _ent + build flags

* fix linters

* delete duplicate files

* fix changelog

* rename test files

* fix linter

* try to bypass false positive linter err

* fix

* Rename file

* fix linter

* fix linter

* remove go:build enterprise commends from _ent files

* update order statuses to use kebab case + fix scanner failures

* add missing order status

* Template Integration For pki_external_ca resources (#13069)

* Implement CA manager for ACME-based workflows

* Add a runtime component for pkiexternalca

* make fmt

* refactor tests into table tests

* update with suggestions

* initial commit

* fix test failure

* changes

* remove logger check

* remove redundant config by name check

* convert to table tests

* added comments

* updates

* Fix tests

* fix nil pointer issue

* move changes to _ent files

* remove ce duplicate files

* updates

* update template.go

* added changelog.txt

* create template_pem_ent_test.go

* added comment explanation

* update ca_manager_ent.go

* update changelog

* separate ce stubs into server_ce.go and common code into server.go

* Moved helper functions to bottom of test file. Added godocs.

* Make pkiExternalCA name required in template

* remove go:build enterprise commends from _ent files

* rename to template_pem_ent

* include ent tag in server_ent.go

* remove enterprise tag comment from server_ent.go

* create pki_external_config_ce.go

* update template_pem_ent_integration_test.go

* rename integration test

---------




---------

Co-authored-by: Jaired Jawed <jaired.jawed@hashicorp.com>
Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com>
Co-authored-by: Zlaticanin <60530402+Zlaticanin@users.noreply.github.com>
Co-authored-by: Milena Zlaticanin <Milena.Zlaticanin@ibm.com>
2026-03-25 09:43:27 -04:00

352 lines
11 KiB
Go

// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
// Package template is responsible for rendering user supplied templates to
// disk. The Server type accepts configuration to communicate to a Vault server
// and a Vault token for authentication. Internally, the Server creates a Consul
// Template Runner which manages reading secrets from Vault and rendering
// templates to disk at configured locations
package template
import (
"context"
"errors"
"fmt"
"io"
"math"
"strings"
sync "sync/atomic"
"text/template"
"time"
ctconfig "github.com/hashicorp/consul-template/config"
"github.com/hashicorp/consul-template/manager"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/config"
"github.com/hashicorp/vault/command/agent/internal/ctmanager"
"github.com/hashicorp/vault/helper/useragent"
"github.com/hashicorp/vault/sdk/helper/backoff"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pointerutil"
"github.com/hashicorp/vault/sdk/logical"
"go.uber.org/atomic"
)
// ServerConfig is a config struct for setting up the basic parts of the
// Server
type ServerConfig struct {
Logger hclog.Logger
// Client *api.Client
AgentConfig *config.Config
// PKIExternalCAServer provides access to named external CA material for templates.
PKIExternalCAServer PKIExternalCAProvider
ExitAfterAuth bool
Namespace string
// LogLevel is needed to set the internal Consul Template Runner's log level
// to match the log level of Vault Agent. The internal Runner creates it's own
// logger and can't be set externally or copied from the Template Server.
//
// LogWriter is needed to initialize Consul Template's internal logger to use
// the same io.Writer that Vault Agent itself is using.
LogLevel hclog.Level
LogWriter io.Writer
}
// PKIExternalCAProvider returns template-ready data for named external CA entries.
type PKIExternalCAProvider interface {
TemplatePEMByName(name string) (any, error)
CertIssuedCh() <-chan struct{}
}
// Server manages the Consul Template Runner which renders templates
type Server struct {
// config holds the ServerConfig used to create it. It's passed along in other
// methods
config *ServerConfig
// runner is the consul-template runner
runner *manager.Runner
runnerStarted *atomic.Bool
// Templates holds the parsed Consul Templates
Templates []*ctconfig.TemplateConfig
// lookupMap is a list of templates indexed by their consul-template ID. This
// is used to ensure all Vault templates have been rendered before returning
// from the runner in the event we're using exit after auth.
lookupMap map[string][]*ctconfig.TemplateConfig
DoneCh chan struct{}
stopped *atomic.Bool
logger hclog.Logger
exitAfterAuth bool
}
// NewServer returns a new configured server
func NewServer(conf *ServerConfig) *Server {
ts := Server{
DoneCh: make(chan struct{}),
stopped: atomic.NewBool(false),
runnerStarted: atomic.NewBool(false),
logger: conf.Logger,
config: conf,
exitAfterAuth: conf.ExitAfterAuth,
}
return &ts
}
// Run kicks off the internal Consul Template runner, and listens for changes to
// the token from the AuthHandler. If Done() is called on the context, shut down
// the Runner and return
func (ts *Server) Run(ctx context.Context, incoming chan string, templates []*ctconfig.TemplateConfig, tokenRenewalInProgress *sync.Bool, invalidTokenCh chan error) error {
if incoming == nil {
return errors.New("template server: incoming channel is nil")
}
latestToken := new(string)
ts.logger.Info("starting template server")
defer func() {
ts.logger.Info("template server stopped")
}()
// If there are no templates, we wait for context cancellation and then return
if len(templates) == 0 {
ts.logger.Info("no templates found")
<-ctx.Done()
return nil
}
ts.applyPKIExternalCAFuncs(templates)
// certIssuedCh fires whenever a PKI external CA issues or renews a cert,
// signalling that templates should be re-rendered.
var certIssuedCh <-chan struct{}
if ts.config.PKIExternalCAServer != nil {
certIssuedCh = ts.config.PKIExternalCAServer.CertIssuedCh()
}
// construct a consul template vault config based the agents vault
// configuration
var runnerConfig *ctconfig.Config
var runnerConfigErr error
managerConfig := ctmanager.ManagerConfig{
AgentConfig: ts.config.AgentConfig,
Namespace: ts.config.Namespace,
LogLevel: ts.config.LogLevel,
LogWriter: ts.config.LogWriter,
}
runnerConfig, runnerConfigErr = ctmanager.NewConfig(managerConfig, templates)
if runnerConfigErr != nil {
return fmt.Errorf("template server failed to runner generate config: %w", runnerConfigErr)
}
var err error
ts.runner, err = manager.NewRunner(runnerConfig, false)
if err != nil {
return fmt.Errorf("template server failed to create: %w", err)
}
// Build the lookup map using the id mapping from the Template runner. This is
// used to check the template rendering against the expected templates. This
// returns a map with a generated ID and a slice of templates for that id. The
// slice is determined by the source or contents of the template, so if a
// configuration has multiple templates specified, but are the same source /
// contents, they will be identified by the same key.
idMap := ts.runner.TemplateConfigMapping()
lookupMap := make(map[string][]*ctconfig.TemplateConfig, len(idMap))
for id, ctmpls := range idMap {
for _, ctmpl := range ctmpls {
tl := lookupMap[id]
tl = append(tl, ctmpl)
lookupMap[id] = tl
}
}
ts.lookupMap = lookupMap
// Create backoff object to calculate backoff time before restarting a failed
// consul template server
restartBackoff := backoff.NewBackoff(math.MaxInt, consts.DefaultMinBackoff, consts.DefaultMaxBackoff)
for {
select {
case <-ctx.Done():
ts.runner.Stop()
return nil
case token := <-incoming:
if token != *latestToken {
ts.logger.Info("template server received new token")
// If the runner was previously started and we intend to exit
// after auth, do not restart the runner if a new token is
// received.
if ts.exitAfterAuth && ts.runnerStarted.Load() {
ts.logger.Info("template server not restarting with new token with exit_after_auth set to true")
continue
}
ts.runner.Stop()
*latestToken = token
ctv := ctconfig.Config{
Vault: &ctconfig.VaultConfig{
Token: latestToken,
ClientUserAgent: pointerutil.StringPtr(useragent.AgentTemplatingString()),
},
}
runnerConfig = runnerConfig.Merge(&ctv)
var runnerErr error
ts.runner, runnerErr = manager.NewRunner(runnerConfig, false)
if runnerErr != nil {
ts.logger.Error("template server failed with new Vault token", "error", runnerErr)
continue
}
ts.runnerStarted.CAS(false, true)
go ts.runner.Start()
}
case <-certIssuedCh:
// A PKI external CA issued or renewed a certificate; restart the runner
// so templates that use pkiCertExternalCa pick up the new material.
if !ts.runnerStarted.Load() {
continue
}
ts.logger.Info("template server restarting runner: PKI external CA certificate updated")
ts.runner.Stop()
var runnerCertErr error
ts.runner, runnerCertErr = manager.NewRunner(runnerConfig, false)
if runnerCertErr != nil {
ts.logger.Error("template server failed to restart runner after cert issuance", "error", runnerCertErr)
continue
}
go ts.runner.Start()
case err := <-ts.runner.ErrCh:
ts.logger.Error("template server error", "error", err.Error())
ts.runner.StopImmediately()
// Return after stopping the runner if exit on retry failure was
// specified
if ts.config.AgentConfig.TemplateConfig != nil && ts.config.AgentConfig.TemplateConfig.ExitOnRetryFailure {
return fmt.Errorf("template server: %w", err)
}
// Calculate the amount of time to backoff using exponential backoff
sleep, err := restartBackoff.Next()
if err != nil {
ts.logger.Error("template server: reached maximum number of restart attempts")
restartBackoff.Reset()
}
// Sleep for the calculated backoff time then attempt to create a new runner
ts.logger.Warn(fmt.Sprintf("template server restart: retry attempt after %s", sleep))
time.Sleep(sleep)
ts.runner, err = manager.NewRunner(runnerConfig, false)
if err != nil {
return fmt.Errorf("template server failed to create: %w", err)
}
go ts.runner.Start()
case <-ts.runner.TemplateRenderedCh():
// A template has been rendered, figure out what to do
events := ts.runner.RenderEvents()
// events are keyed by template ID, and can be matched up to the id's from
// the lookupMap
if len(events) < len(ts.lookupMap) {
// Not all templates have been rendered yet
continue
}
// assume the renders are finished, until we find otherwise
doneRendering := true
for _, event := range events {
// This template hasn't been rendered
if event.LastWouldRender.IsZero() {
doneRendering = false
}
}
if doneRendering && ts.exitAfterAuth {
// if we want to exit after auth, go ahead and shut down the runner and
// return. The deferred closing of the DoneCh will allow agent to
// continue with closing down
ts.runner.Stop()
return nil
}
case err := <-ts.runner.ServerErrCh:
var responseError *api.ResponseError
ok := errors.As(err, &responseError)
if !ok {
ts.logger.Error("template server: could not extract error response")
continue
}
if responseError.StatusCode == 403 && strings.Contains(responseError.Error(), logical.ErrInvalidToken.Error()) && !tokenRenewalInProgress.Load() {
ts.logger.Info("template server: received invalid token error")
// Drain the error channel and incoming channel before sending a new error
select {
case <-invalidTokenCh:
case <-incoming:
default:
}
invalidTokenCh <- err
}
}
}
}
// applyPKIExternalCAFuncs injects the pkiCertExternalCa template function into
// each template's ExtFuncMap. The CA name must always be passed explicitly as an
// argument: {{ pkiCertExternalCa "ca-name" }}.
func (ts *Server) applyPKIExternalCAFuncs(templates []*ctconfig.TemplateConfig) {
if ts.config == nil || ts.config.PKIExternalCAServer == nil {
return
}
for _, tmpl := range templates {
if tmpl.ExtFuncMap == nil {
tmpl.ExtFuncMap = make(template.FuncMap)
}
tmpl.ExtFuncMap["pkiCertExternalCa"] = pkiCertExternalCaFunc(ts.config.PKIExternalCAServer)
}
}
func pkiCertExternalCaFunc(server PKIExternalCAProvider) func(...string) (interface{}, error) {
return func(args ...string) (interface{}, error) {
if server == nil {
return nil, fmt.Errorf("pki external ca server is not configured")
}
if len(args) != 1 {
return nil, fmt.Errorf("pkiCertExternalCa requires exactly one argument: the pki_external_ca name")
}
name := strings.TrimSpace(args[0])
if name == "" {
return nil, fmt.Errorf("pkiCertExternalCa requires a non-empty pki_external_ca name")
}
pem, err := server.TemplatePEMByName(name)
if err != nil {
return nil, err
}
if pem == nil {
// Certificate not yet available; return nil so {{ with }} skips the block.
return nil, nil
}
return pem, nil
}
}
func (ts *Server) Stop() {
if ts.stopped.CAS(false, true) {
close(ts.DoneCh)
}
}