Files
teleport/lib/autoupdate/agent/config.go
T
Angel Dionisio 15136eb0d8 Sign managed updates Linux tarballs with cosign (#68053)
* add managed updates artifact signature verification

* updating signature max size to a more realistic, yet generous size

* fail teleport-update bulds when artifact verifier key is missing

* forward artifact signing key into Docker buildbox

* verify artifact signature over stream instead of pre-computed digest

* add insecure-skip-artifact-signature flag for dev builds

* renaming insecure signature flag and simplify verifier flow

* import separation for linter CI fix

* gofmt

* simplifying verifyArfifactSig function, table tests

* verify artifact signature during install, adding godoc for WithDigest

* adding staging CDN fallback

* updating isStagingCDN url check by adding helper function to check for raw vs want url equality, pr comments

* clarify ECDSA-specific digest verification comment

* initialize artifact signature verifier at construction time

* embed backup artifact signature public key for managed updates

* move teleport-update signature inputs to main.go

* removing comment

* verify staging signatures by default with explicit fallback

* update staging signature verification to trust prod artifacts

* fix test expected message error
2026-08-07 20:05:42 +00:00

298 lines
9.2 KiB
Go

/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package agent
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"runtime"
"strings"
"time"
"github.com/gravitational/trace"
"gopkg.in/yaml.v3"
"github.com/gravitational/teleport/lib/autoupdate"
)
const (
// defaultSetting is used to represent the default value for updater config.
defaultSetting = "default"
)
const (
// UpdateConfigName specifies the name of the file inside versionsDirName containing configuration for the teleport update.
UpdateConfigName = "update.yaml"
// UpdateConfigV1 specifies the version of the update.yaml file.
UpdateConfigV1 = "v1"
// UpdateConfigKind specifies the kind of the update.yaml file.
UpdateConfigKind = "update_config"
)
// UpdateConfig describes the update.yaml file schema.
// Pointer values should be replaced and not mutated, see Copy.
type UpdateConfig struct {
// Version of the configuration file
Version string `yaml:"version"`
// Kind of configuration file (always "update_config")
Kind string `yaml:"kind"`
// Spec contains user-specified configuration.
Spec UpdateSpec `yaml:"spec"`
// Status contains state configuration.
Status UpdateStatus `yaml:"status"`
}
// Copy an UpdateConfig. Pointers are not copied.
func (cfg *UpdateConfig) Copy() *UpdateConfig {
if cfg == nil {
return &UpdateConfig{}
}
return toPtr(*cfg)
}
// UpdateSpec describes the spec field in update.yaml.
// Pointer values should be replaced and not mutated, see Copy.
type UpdateSpec struct {
// Proxy address
Proxy string `yaml:"proxy"`
// Path is the location the Teleport binaries are linked into.
Path string `yaml:"path"`
// Group specifies the update group identifier for the agent.
Group string `yaml:"group,omitempty"`
// BaseURL is CDN base URL used for the Teleport tgz download URL.
BaseURL string `yaml:"base_url,omitempty"`
// InsecureSkipSignatureVerify disables artifact signature verification and falls back
// to checksum-only verification. This is intended for development builds that are not signed.
InsecureSkipSignatureVerify bool `yaml:"insecure_skip_signature_verify,omitempty"`
// EnableStagingSignatureVerify enforces detached signature verification for staging CDN artifacts
// that would otherwise skip signature verification.
EnableStagingSignatureVerify bool `yaml:"enable_staging_signature_verify,omitempty"`
// Enabled controls whether auto-updates are enabled.
Enabled bool `yaml:"enabled"`
// Pinned controls whether the active_version is pinned.
Pinned bool `yaml:"pinned"`
// SELinuxSSH controls whether an SELinux module will be installed to
// constrain Teleport SSH.
SELinuxSSH bool `yaml:"selinux_ssh,omitempty"`
}
// UpdateStatus describes the status field in update.yaml.
// Pointer values should be replaced and not mutated, see Copy.
type UpdateStatus struct {
// IDFile is the path to a temporary file containing the updater ID.
IDFile string `yaml:"id_file,omitempty"`
// LastUpdate status, if attempted
LastUpdate *LastUpdate `yaml:"last_update,omitempty"`
// Active is the currently active revision of Teleport.
Active Revision `yaml:"active"`
// Backup is the last working revision of Teleport.
Backup *Revision `yaml:"backup,omitempty"`
// Skip is the skipped revision of Teleport.
// Skipped revisions are not applied because they
// are known to crash.
Skip *Revision `yaml:"skip,omitempty"`
}
// LastUpdate describes the last attempted updated.
type LastUpdate struct {
// Success or failure of the attempted update
Success bool `yaml:"success"`
// Time the update occurred
Time time.Time `yaml:"time"`
// Target revision for the update
Target Revision `yaml:"target"`
}
// Revision is a version and edition of Teleport.
type Revision struct {
// Version is the version of Teleport.
Version string `yaml:"version" json:"version"`
// Flags describe the edition of Teleport.
Flags autoupdate.InstallFlags `yaml:"flags,flow,omitempty" json:"flags,omitempty"`
}
// NewRevision create a Revision.
// If version is not set, no flags are returned.
// This ensures that all Revisions without versions are zero-valued.
func NewRevision(version string, flags autoupdate.InstallFlags) Revision {
if version != "" {
return Revision{
Version: version,
Flags: flags,
}
}
return Revision{}
}
// NewRevisionFromDir translates a directory path containing Teleport into a Revision.
func NewRevisionFromDir(dir string) (Revision, error) {
parts := strings.Split(dir, "_")
var out Revision
if len(parts) == 0 {
return out, trace.Errorf("dir name empty")
}
out.Version = parts[0]
if out.Version == "" {
return out, trace.Errorf("version missing in dir %s", dir)
}
switch flags := parts[1:]; len(flags) {
case 2:
if flags[1] != autoupdate.FlagFIPS.DirFlag() {
break
}
out.Flags |= autoupdate.FlagFIPS
fallthrough
case 1:
if flags[0] != autoupdate.FlagEnterprise.DirFlag() {
break
}
out.Flags |= autoupdate.FlagEnterprise
fallthrough
case 0:
return out, nil
}
return out, trace.Errorf("invalid flag in %s", dir)
}
// Dir returns the directory path name of a Revision.
// These are unambiguous for semver and may be parsed with NewRevisionFromDir.
func (r Revision) Dir() string {
// Do not change the order of these statements.
// Otherwise, installed versions will no longer match update.yaml.
var suffix string
if r.Flags&(autoupdate.FlagEnterprise|autoupdate.FlagFIPS) != 0 {
suffix += "_" + autoupdate.FlagEnterprise.DirFlag()
}
if r.Flags&autoupdate.FlagFIPS != 0 {
suffix += "_" + autoupdate.FlagFIPS.DirFlag()
}
return r.Version + suffix
}
// String returns a human-readable description of a Teleport revision.
// These are semver-ambiguous and should not be parsed.
func (r Revision) String() string {
if flags := r.Flags.Strings(); len(flags) > 0 {
return fmt.Sprintf("%s+%s", r.Version, strings.Join(flags, "+"))
}
return r.Version
}
// readConfig reads UpdateConfig from a file.
func readConfig(path string) (*UpdateConfig, error) {
f, err := os.Open(path)
if errors.Is(err, fs.ErrNotExist) {
return &UpdateConfig{
Version: UpdateConfigV1,
Kind: UpdateConfigKind,
}, nil
}
if err != nil {
return nil, trace.Wrap(err, "failed to open")
}
defer f.Close()
var cfg UpdateConfig
if err := yaml.NewDecoder(f).Decode(&cfg); err != nil {
return nil, trace.Wrap(err, "failed to parse")
}
if k := cfg.Kind; k != UpdateConfigKind {
return nil, trace.Errorf("invalid kind %s", k)
}
if v := cfg.Version; v != UpdateConfigV1 {
return nil, trace.Errorf("invalid version %s", v)
}
return &cfg, nil
}
// writeConfig writes UpdateConfig to a file atomically, ensuring the file cannot be corrupted.
func writeConfig(filename string, cfg *UpdateConfig) error {
return trace.Wrap(writeAtomicWithinDir(filename, configFileMode, func(w io.Writer) error {
return trace.Wrap(yaml.NewEncoder(w).Encode(cfg))
}))
}
func updateConfigSpec(spec *UpdateSpec, override OverrideConfig) error {
if override.Proxy != "" {
spec.Proxy = override.Proxy
}
if override.Path != "" {
spec.Path = override.Path
}
spec.Group = overrideOptional(spec.Group, override.Group)
spec.BaseURL = overrideOptional(spec.BaseURL, override.BaseURL)
if spec.BaseURL != "" &&
!strings.HasPrefix(strings.ToLower(spec.BaseURL), "https://") {
return trace.Errorf("Teleport download base URL %s must use TLS (https://)", spec.BaseURL)
}
if override.InsecureSkipSignatureVerifyChanged {
spec.InsecureSkipSignatureVerify = override.InsecureSkipSignatureVerify
}
if override.EnableStagingSignatureVerifyChanged {
spec.EnableStagingSignatureVerify = override.EnableStagingSignatureVerify
}
if override.Enabled {
spec.Enabled = true
}
if override.Pinned {
spec.Pinned = true
}
if override.SELinuxSSHChanged {
spec.SELinuxSSH = override.SELinuxSSH
}
if spec.SELinuxSSH && runtime.GOOS != "linux" {
return trace.BadParameter("SELinux is only supported on Linux")
}
return nil
}
func overrideOptional(orig, override string) string {
switch override {
case "":
return orig
case defaultSetting:
return ""
default:
return override
}
}
// Status of the agent auto-updates system.
type Status struct {
UpdateSpec `yaml:",inline"`
UpdateStatus `yaml:",inline"`
FindResp `yaml:",inline"`
// ID is the updater ID.
ID string `yaml:"id,omitempty"`
}
// FindResp summarizes the auto-update status response from cluster.
type FindResp struct {
// Target revision of Teleport to install
Target Revision `yaml:"target"`
// InWindow is true when the install should happen now.
InWindow bool `yaml:"in_window"`
// Jitter duration before an automated install
Jitter time.Duration `yaml:"jitter"`
// AGPL installations cannot use the official CDN.
AGPL bool `yaml:"agpl,omitempty"`
}