Skip to content

Commit

Permalink
Improve registration updates
Browse files Browse the repository at this point in the history
- Store emulated TPM seed for future registrations
- Exit with error code in case of failures (systemd will manage restarts)
- Remove support for multiple configuration files
  • Loading branch information
anmazzotti committed Jul 11, 2023
1 parent 07dfe06 commit 90ec8bc
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 159 deletions.
180 changes: 59 additions & 121 deletions cmd/register/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/mudler/yip/pkg/schema"
agent "github.com/rancher/system-agent/pkg/config"
Expand All @@ -42,13 +41,17 @@ import (
)

const (
stateInstallFile = "/run/initramfs/cos-state/state.yaml"
stateRegistrationFile = "/oem/registration/state.yaml"
agentStateDir = "/var/lib/elemental/agent"
agentConfDir = "/etc/rancher/elemental/agent"
afterInstallHook = "/oem/install-hook.yaml"
regConfDir = "/oem/registration"
liveRegConfDir = "/run/initramfs/live"
stateInstallFile = "/run/initramfs/cos-state/state.yaml"
agentStateDir = "/var/lib/elemental/agent"
agentConfDir = "/etc/rancher/elemental/agent"
afterInstallHook = "/oem/install-hook.yaml"

// Registration config directories, depending if system is live or not
regConfExt = "yaml"
regConfDir = "/oem/registration"
regConfName = "config"
liveRegConfDir = "/run/initramfs/live"
liveRegConfName = "livecd-cloud-config"

// This file stores the registration URL and certificate used for the registration
// this file will be stored into the install system by an after-install hook
Expand All @@ -74,35 +77,25 @@ func main() {

log.Infof("Register version %s, commit %s, commit date %s", version.Version, version.Commit, version.CommitDate)

// Locate config directory and file
var configDir string
var configName string
if len(args) == 0 {
args = append(args, getRegistrationConfigDir())
}

for _, arg := range args {
_, err := os.Stat(arg)
if err != nil {
log.Warningf("cannot access config path %s: %s", arg, err.Error())
continue
if !isSystemInstalled() {
configDir = liveRegConfDir
configName = liveRegConfName
} else {
configDir = regConfDir
configName = regConfName
}
} else {
configDir = args[0] //Take the first argument only, ignore the rest
configName = regConfName
}

log.Debugf("scanning config path %s", arg)

files, err := os.ReadDir(arg)
if err != nil {
log.Warningf("cannot read config path contents %s: %s", arg, err.Error())
continue
}
viper.AddConfigPath(arg)
for _, f := range files {
if filepath.Ext(f.Name()) == ".yaml" {
viper.SetConfigType("yaml")
viper.SetConfigName(f.Name())
if err := viper.MergeInConfig(); err != nil {
log.Fatalf("failed to read config %s: %s", f.Name(), err)
}
log.Infof("reading config file %s", f.Name())
}
}
// Merge configuration from file
if err := mergeConfigFromFile(configDir, configName); err != nil {
log.Fatalf("Could not read configuration in directory '%s': %s", configDir, err)
}

if err := viper.Unmarshal(&cfg); err != nil {
Expand All @@ -111,7 +104,7 @@ func main() {

log.Debugf("input config:\n%s", litter.Sdump(cfg))

run(cfg)
run(configDir, cfg)
},
}

Expand All @@ -131,90 +124,59 @@ func main() {
}
}

func run(config elementalv1.Config) {
func mergeConfigFromFile(path string, name string) error {
log.Debugf("Using configuration in directory: %s\n", path)
viper.AddConfigPath(path)
viper.SetConfigName(name)
viper.SetConfigType(regConfExt)
return viper.MergeInConfig()
}

func run(configDir string, config elementalv1.Config) {
// Validate Registration config
registration := config.Elemental.Registration

if registration.URL == "" {
log.Fatal("Registration URL is empty")
}

var (
err error
data, caCert []byte
)

/* Here we can have a file path or the cert data itself */
_, err = os.Stat(registration.CACert)
if err == nil {
log.Info("CACert passed as a file")
caCert, err = os.ReadFile(registration.CACert)
if err != nil {
log.Error(err)
}
} else {
if registration.CACert == "" {
log.Warning("CACert is empty")
}
caCert = []byte(registration.CACert)
}

isRegistrationUpdate := isRegistrationUpdate()
if isRegistrationUpdate {
if isUsingRandomEmulatedTPM(registration) {
log.Error("TPM emulation is active and using a randomized seed, registration update is not supported")
return
}
log.Debugln("Attempting to update registration...")
} else {
log.Debugln("Attempting to perform first time registration...")
caCert, err := getRegistrationCA(registration)
if err != nil {
log.Fatalf("Could not load registration CA certificate: %s", err)
}

for {
data, err = register.Register(registration, caCert, isRegistrationUpdate)
if err != nil {
log.Errorf("failed to register machine inventory: %w", err)
if isRegistrationUpdate {
log.Debugln("Registration update failed, will not retry again")
break
}
time.Sleep(time.Second * 5)
continue
}
client := register.NewClient(configDir)

log.Debugf("Fetched configuration from manager cluster:\n%s\n\n", string(data))
data, err := client.Register(registration, caCert)
if err != nil {
log.Fatalf("failed to register machine inventory: %w", err)
}

if err := yaml.Unmarshal(data, &config); err != nil {
log.Errorf("failed to parse registration configuration: %w", err)
if isRegistrationUpdate {
break
}
time.Sleep(time.Second * 5)
continue
}
log.Debugf("Fetched configuration from manager cluster:\n%s\n\n", string(data))

break
if err := yaml.Unmarshal(data, &config); err != nil {
log.Errorf("failed to parse registration configuration: %w", err)
}

if !isSystemInstalled() {
if err := installElemental(config); err != nil {
log.Fatal("elemental installation failed: ", err)
log.Fatalf("elemental installation failed: %w", err)
}

log.Info("elemental installation completed, please reboot")
}

if err := updateRegistrationState(); err != nil {
log.Errorf("failed to update registration state file %s: %w", stateRegistrationFile, err)
}
}

func getRegistrationConfigDir() string {
if isSystemInstalled() {
log.Debugf("System is not running live, using configuration in directory: %s\n", regConfDir)
return regConfDir
func getRegistrationCA(registration elementalv1.Registration) ([]byte, error) {
/* Here we can have a file path or the cert data itself */
if _, err := os.Stat(registration.CACert); err == nil {
log.Info("CACert passed as a file")
return os.ReadFile(registration.CACert)
}
if registration.CACert == "" {
log.Warning("CACert is empty")
}
log.Debugf("Using live configuration in directory: %s\n", liveRegConfDir)
return liveRegConfDir
return []byte(registration.CACert), nil
}

func installElemental(config elementalv1.Config) error {
Expand Down Expand Up @@ -406,27 +368,3 @@ func writeSystemAgentConfig(config elementalv1.Elemental) (string, error) {

return f.Name(), err
}

func isUsingRandomEmulatedTPM(config elementalv1.Registration) bool {
return config.EmulateTPM && config.EmulatedTPMSeed == elementalv1.TPMRandomSeedValue
}

func isRegistrationUpdate() bool {
_, err := os.Stat(stateRegistrationFile)
return err == nil
}

func updateRegistrationState() error {
if _, err := os.Stat(regConfDir); os.IsNotExist(err) {
log.Debugf("Registration config dir '%s' does not exist. Creating now.", regConfDir)
if err := os.MkdirAll(regConfDir, 0700); err != nil {
return fmt.Errorf("creating registration config directory: %w", err)
}
}
file, err := os.Create(stateRegistrationFile)
if err != nil {
return fmt.Errorf("creating registration state file: %w", err)
}
defer file.Close()
return nil
}
70 changes: 53 additions & 17 deletions pkg/register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import (
"github.com/rancher/elemental-operator/pkg/tpm"
)

type Client interface {
Register(reg elementalv1.Registration, caCert []byte) ([]byte, error)
}

type authClient interface {
Init(reg elementalv1.Registration) error
GetName() string
Expand All @@ -46,25 +50,30 @@ type authClient interface {
Authenticate(conn *websocket.Conn) error
}

var _ Client = (*client)(nil)

type client struct {
stateHandler StateHandler
}

func NewClient(configDir string) Client {
return &client{
stateHandler: NewFileStateHandler(configDir),
}
}

// Register attempts to register the machine with the elemental-operator.
// If the machine is already installed and registered, a registration can still be attempted turning the `isUpdate` flag on.
// Registration updates will fetch and apply new labels, and update Machine annotations such as the IP address.
func Register(reg elementalv1.Registration, caCert []byte, isUpdate bool) ([]byte, error) {
var auth authClient

switch reg.Auth {
case "tpm":
auth = &tpm.AuthClient{}
case "mac":
auth = &plainauth.AuthClient{}
case "sys-uuid":
auth = &plainauth.AuthClient{}
default:
return nil, fmt.Errorf("unsupported authentication: %s", reg.Auth)
func (r *client) Register(reg elementalv1.Registration, caCert []byte) ([]byte, error) {
state, err := r.stateHandler.Load()
if err != nil {
return nil, fmt.Errorf("loading registration state: %w", err)
}

if err := auth.Init(reg); err != nil {
return nil, fmt.Errorf("init %s authentication: %w", auth.GetName(), err)
auth, err := getAuthenticator(reg, &state)
if err != nil {
return nil, fmt.Errorf("initializing authenticator: %w", err)
}

log.Infof("Connect to %s", reg.URL)
Expand All @@ -87,14 +96,17 @@ func Register(reg elementalv1.Registration, caCert []byte, isUpdate bool) ([]byt
}
log.Infof("Negotiated protocol version: %d", protoVersion)

if isUpdate {
if state.IsUpdatable() {
if protoVersion < MsgUpdate {
return nil, errors.New("elemental-operator protocol version does not support update")
}
log.Debugln("Initiating registration update")
if err := sendUpdateData(conn); err != nil {
return nil, fmt.Errorf("failed to send update data: %w", err)
}
state.lastUpdate = time.Now()
} else {
state.initialRegistration = time.Now()
}

if !reg.NoSMBIOS {
Expand All @@ -118,6 +130,9 @@ func Register(reg elementalv1.Registration, caCert []byte, isUpdate bool) ([]byt
}
}

log.Info("Saving registration state")
r.stateHandler.Save(state)

Check failure on line 134 in pkg/register/register.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `r.stateHandler.Save` is not checked (errcheck)

log.Info("Get elemental configuration")
if err := WriteMessage(conn, MsgGet, []byte{}); err != nil {
return nil, fmt.Errorf("request elemental configuration: %w", err)
Expand All @@ -128,11 +143,32 @@ func Register(reg elementalv1.Registration, caCert []byte, isUpdate bool) ([]byt
}

// Support old Elemental Operator (<= v1.1.0)
_, r, err := conn.NextReader()
_, reader, err := conn.NextReader()
if err != nil {
return nil, fmt.Errorf("read elemental configuration: %w", err)
}
return io.ReadAll(r)
return io.ReadAll(reader)
}

func getAuthenticator(reg elementalv1.Registration, state *State) (authClient, error) {
var auth authClient
switch reg.Auth {
case "tpm":
state.emulatedTPMSeed = tpm.GetTPMSeed(reg, state.emulatedTPM, state.emulatedTPMSeed)
state.emulatedTPM = reg.EmulateTPM
auth = tpm.NewAuthClient(state.emulatedTPMSeed)
case "mac":
auth = &plainauth.AuthClient{}
case "sys-uuid":
auth = &plainauth.AuthClient{}
default:
return nil, fmt.Errorf("unsupported authentication: %s", reg.Auth)
}

if err := auth.Init(reg); err != nil {
return nil, fmt.Errorf("init %s authentication: %w", auth.GetName(), err)
}
return auth, nil
}

func initWebsocketConn(url string, caCert []byte, auth authClient) (*websocket.Conn, error) {
Expand Down
Loading

0 comments on commit 90ec8bc

Please sign in to comment.