Skip to content

Commit

Permalink
Implement authentication (#8)
Browse files Browse the repository at this point in the history
* Implement registration and host authentication

Signed-off-by: Andrea Mazzotti <[email protected]>
  • Loading branch information
anmazzotti authored Nov 13, 2023
1 parent 9952df2 commit e3cb882
Show file tree
Hide file tree
Showing 34 changed files with 1,463 additions and 231 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ You can use it with any OpenAPI compliant tool, for example the online [Swagger

This API is consumed by the `elemental-agent` and is meant for **Internal** use only.

### Authentication & Authorization

The Elemental API uses two different authorization header.
The `Registration-Authorization` should contain a valid JWT formatted `Bearer` token when fetching registration information or registering a new host.
When registering a new host and modifying a host resource, the `Authorization` header should also contain a valid JWT formatted `Bearer` token that identifies the host.
For more information, you can find more details in the [documentation](doc/AUTH.md).

## Rancher Integration

[Rancher Turtles](https://docs.rancher-turtles.com/) is an extension to Rancher that brings increased integration with Cluster API.
Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/elementalhost_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ElementalHostSpec struct {
// using this host.
// +optional
MachineRef *corev1.ObjectReference `json:"machineRef,omitempty"`
// PubKey is the host public key to verify when authenticating
// Elemental API requests for this host.
PubKey string `json:"pubKey,omitempty"`
}

// ElementalHostStatus defines the observed state of ElementalHost.
Expand Down
7 changes: 7 additions & 0 deletions api/v1beta1/elementalregistration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1beta1
import (
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
Expand All @@ -34,6 +35,8 @@ type ElementalRegistrationSpec struct {
// Config points to Elemental machine configuration.
// +optional
Config Config `json:"config,omitempty"`
// PrivateKeyRef is a reference to a secret containing the private key used to generate registration tokens
PrivateKeyRef *corev1.ObjectReference `json:"privateKeyRef,omitempty"`
}

// ElementalRegistrationStatus defines the observed state of ElementalRegistration.
Expand Down Expand Up @@ -78,6 +81,10 @@ type Registration struct {
URI string `json:"uri,omitempty" yaml:"uri,omitempty" mapstructure:"uri"`
// +optional
CACert string `json:"caCert,omitempty" yaml:"caCert,omitempty" mapstructure:"caCert"`
// +optional
TokenDuration time.Duration `json:"tokenDuration,omitempty" yaml:"tokenDuration,omitempty" mapstructure:"tokenDuration"`
// +optional
Token string `json:"token,omitempty" yaml:"token,omitempty" mapstructure:"token"`
}

type Agent struct {
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion cmd/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ registration:
WhfJrSPzvfWPO73w0MFMBRXZ74Tc24SN6QPBin5LaAIhAM9hidFQ71SZQnPY3Y1I
JZPkAoVeIOoFDgXvl9MkHBuk
-----END CERTIFICATE-----
# A valid JWT token to use during registration
token: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJFbGVtZW50YWxSZWdpc3RyYXRpb25SZWNvbmNpbGVyIiwic3ViIjoiaHR0cDovLzE5Mi4xNjguMTIyLjEwOjMwMDA5L2VsZW1lbnRhbC92MS9uYW1lc3BhY2VzL2RlZmF1bHQvcmVnaXN0cmF0aW9ucy9teS1yZWdpc3RyYXRpb24iLCJhdWQiOlsiaHR0cDovLzE5Mi4xNjguMTIyLjEwOjMwMDA5L2VsZW1lbnRhbC92MS9uYW1lc3BhY2VzL2RlZmF1bHQvcmVnaXN0cmF0aW9ucy9teS1yZWdpc3RyYXRpb24iXSwibmJmIjoxNjk5ODY0NzIwLCJpYXQiOjE2OTk4NjQ3MjB9.YQsYZoaZ3tGV6z5aXo1e9LmGdA-wQOtmmpi4yAAfXcqh6_S6iIjgblXqw6koQJCzhBMy2-APPQL0ANEBcAljBQ
agent:
# Work directory
workDir: /var/lib/elemental/agent
Expand All @@ -77,7 +79,7 @@ agent:
# Enable agent debug logs
debug: false
# Which OS plugin to use
osPlugin: "/usr/lib/elemental/plugins/elemental.so"
osPlugin: /usr/lib/elemental/plugins/elemental.so
# The period used by the agent to sync with the Elemental API
reconciliation: 1m
# Allow 'http' scheme
Expand Down
45 changes: 27 additions & 18 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/client"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/config"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/hostname"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/identity"
log "github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/log"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/utils"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/api"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/identity"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/version"
"github.com/rancher-sandbox/cluster-api-provider-elemental/pkg/agent/osplugin"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -50,7 +50,7 @@ var (
func main() {
fs := vfs.OSFS
osPluginLoader := osplugin.NewLoader()
client := client.NewClient()
client := client.NewClient(version.Version)
commandRunner := utils.NewCommandRunner()
cmd := newCommand(fs, osPluginLoader, commandRunner, client)
if err := cmd.Execute(); err != nil {
Expand Down Expand Up @@ -103,13 +103,13 @@ func newCommand(fs vfs.FS, pluginLoader osplugin.Loader, commandRunner utils.Com
return fmt.Errorf("Initializing plugin: %w", err)
}
// Initialize Identity
identityManager := identity.NewDummyManager(fs, conf.Agent.WorkDir)
signingKey, err := identityManager.LoadSigningKeyOrCreateNew()
identityManager := identity.NewManager(fs, conf.Agent.WorkDir)
identity, err := identityManager.LoadSigningKeyOrCreateNew()
if err != nil {
return fmt.Errorf("initializing identity: %w", err)
}
// Initialize Elemental API Client
if err := client.Init(fs, signingKey, conf); err != nil {
if err := client.Init(fs, identity, conf); err != nil {
return fmt.Errorf("initializing Elemental API client: %w", err)
}
// Get current hostname
Expand All @@ -120,10 +120,14 @@ func newCommand(fs vfs.FS, pluginLoader osplugin.Loader, commandRunner utils.Com
// Register
if registerFlag {
log.Info("Registering Elemental Host")
pubKey, err := identity.MarshalPublic()
if err != nil {
return fmt.Errorf("marshalling host public key: %w", err)
}
var registration *api.RegistrationResponse
hostname, registration = handleRegistration(client, osPlugin, conf.Agent.Reconciliation)
hostname, registration = handleRegistration(client, osPlugin, pubKey, conf.Registration.Token, conf.Agent.Reconciliation)
log.Infof("Successfully registered as '%s'", hostname)
if err := handlePostRegistration(osPlugin, hostname, signingKey, registration); err != nil {
if err := handlePostRegistration(osPlugin, hostname, identity, registration); err != nil {
return fmt.Errorf("handling post registration: %w", err)
}
// Exit program if --install was not called
Expand All @@ -134,7 +138,7 @@ func newCommand(fs vfs.FS, pluginLoader osplugin.Loader, commandRunner utils.Com
// Install
if installFlag {
log.Info("Installing Elemental")
handleInstall(client, osPlugin, hostname, conf.Agent.Reconciliation)
handleInstall(client, osPlugin, hostname, conf.Registration.Token, conf.Agent.Reconciliation)
log.Info("Installation successful")
handlePost(osPlugin, conf.Agent.PostInstall.PowerOff, conf.Agent.PostInstall.Reboot)
return nil
Expand All @@ -143,7 +147,7 @@ func newCommand(fs vfs.FS, pluginLoader osplugin.Loader, commandRunner utils.Com
// Reset
if resetFlag {
log.Info("Resetting Elemental")
handleReset(client, osPlugin, conf.Agent.Reconciliation, hostname)
handleReset(client, osPlugin, hostname, conf.Registration.Token, conf.Agent.Reconciliation)
log.Info("Reset successful")
handlePost(osPlugin, conf.Agent.PostReset.PowerOff, conf.Agent.PostReset.Reboot)
return nil
Expand Down Expand Up @@ -221,7 +225,7 @@ func getConfig(fs vfs.FS) (config.Config, error) {
return conf, nil
}

func handleRegistration(client client.Client, osPlugin osplugin.Plugin, registrationRecoveryPeriod time.Duration) (string, *api.RegistrationResponse) {
func handleRegistration(client client.Client, osPlugin osplugin.Plugin, pubKey []byte, registrationToken string, registrationRecoveryPeriod time.Duration) (string, *api.RegistrationResponse) {
hostnameFormatter := hostname.NewFormatter(osPlugin)
var newHostname string
var registration *api.RegistrationResponse
Expand All @@ -235,7 +239,7 @@ func handleRegistration(client client.Client, osPlugin osplugin.Plugin, registra
}
// Fetch remote Registration
log.Debug("Fetching remote registration")
registration, err = client.GetRegistration()
registration, err = client.GetRegistration(registrationToken)
if err != nil {
log.Error(err, "getting remote Registration")
registrationError = true
Expand All @@ -257,7 +261,8 @@ func handleRegistration(client client.Client, osPlugin osplugin.Plugin, registra
Name: newHostname,
Annotations: registration.HostAnnotations,
Labels: registration.HostLabels,
}); err != nil {
PubKey: string(pubKey),
}, registrationToken); err != nil {
log.Error(err, "registering new ElementalHost")
registrationError = true
continue
Expand All @@ -267,7 +272,7 @@ func handleRegistration(client client.Client, osPlugin osplugin.Plugin, registra
return newHostname, registration
}

func handlePostRegistration(osPlugin osplugin.Plugin, hostnameToSet string, signingKey []byte, registration *api.RegistrationResponse) error {
func handlePostRegistration(osPlugin osplugin.Plugin, hostnameToSet string, id identity.Identity, registration *api.RegistrationResponse) error {
// Persist registered hostname
if err := osPlugin.PersistHostname(hostnameToSet); err != nil {
return fmt.Errorf("persisting hostname '%s': %w", hostnameToSet, err)
Expand All @@ -282,14 +287,18 @@ func handlePostRegistration(osPlugin osplugin.Plugin, hostnameToSet string, sign
return fmt.Errorf("persisting agent config file '%s': %w", configPath, err)
}
// Persist identity file
identityBytes, err := id.Marshal()
if err != nil {
return fmt.Errorf("marshalling identity: %w", err)
}
privateKeyPath := fmt.Sprintf("%s/%s", agentConfig.Agent.WorkDir, identity.PrivateKeyFile)
if err := osPlugin.PersistFile(signingKey, privateKeyPath, 0640, 0, 0); err != nil {
if err := osPlugin.PersistFile(identityBytes, privateKeyPath, 0640, 0, 0); err != nil {
return fmt.Errorf("persisting private key file '%s': %w", privateKeyPath, err)
}
return nil
}

func handleInstall(client client.Client, osPlugin osplugin.Plugin, hostname string, installationRecoveryPeriod time.Duration) {
func handleInstall(client client.Client, osPlugin osplugin.Plugin, hostname string, registrationToken string, installationRecoveryPeriod time.Duration) {
cloudConfigAlreadyApplied := false
alreadyInstalled := false
installationError := false
Expand All @@ -304,7 +313,7 @@ func handleInstall(client client.Client, osPlugin osplugin.Plugin, hostname stri
var err error
if !cloudConfigAlreadyApplied || !alreadyInstalled {
log.Debug("Fetching remote registration")
registration, err = client.GetRegistration()
registration, err = client.GetRegistration(registrationToken)
if err != nil {
log.Error(err, "getting remote Registration")
installationError = true
Expand Down Expand Up @@ -354,7 +363,7 @@ func handleInstall(client client.Client, osPlugin osplugin.Plugin, hostname stri
}
}

func handleReset(client client.Client, osPlugin osplugin.Plugin, resetRecoveryPeriod time.Duration, hostname string) {
func handleReset(client client.Client, osPlugin osplugin.Plugin, hostname string, registrationToken string, resetRecoveryPeriod time.Duration) {
resetError := false
alreadyReset := false
for {
Expand All @@ -375,7 +384,7 @@ func handleReset(client client.Client, osPlugin osplugin.Plugin, resetRecoveryPe
if !alreadyReset {
// Fetch remote Registration
log.Debug("Fetching remote registration")
registration, err := client.GetRegistration()
registration, err := client.GetRegistration(registrationToken)
if err != nil {
log.Error(err, "getting remote Registration")
resetError = true
Expand Down
Loading

0 comments on commit e3cb882

Please sign in to comment.