package hostbox import ( "context" "crypto/x509" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" ) // Actor is the verified projection of the sole control/host-actors.json roster. // Roster authenticates that document and its revision before returning a row. type Actor struct { FleetHomeRegion string HomeRegion string ControlBucket string Box, Enrollment string Revision uint64 Region, Bucket, Namespace string Cloud CloudIdentity Eligible bool Quarantined bool // Binding is hostactors.BoxBinding of this box's row in the roster read // that produced the actor: the digest installed state is stamped with and // compared against. Unlike Revision, the roster's write counter, it moves // only when this box's identity or state or the deployment's binding facts // change (a quarantine, a readmission, a re-enrolment, a floor publish), // never on holds, activations or other boxes' rows. Binding string } // Bound returns the actor with the roster write counter cleared: the part of // the actor that names one enrollment under one binding. Persist this, not the // actor, wherever a later comparison must survive unrelated roster writes. func (a Actor) Bound() Actor { a.Revision = 0 return a } // SameBinding reports whether two actors are the same enrollment under the // same binding. Every field but Revision must match; Binding carries the // quarantine revision and every other bound fact, so a box quarantined and // readmitted since a is never SameBinding with it. func SameBinding(a, b Actor) bool { return a.Bound() == b.Bound() } type CloudIdentity struct{ Provider, Account, Region, InstanceID string } type Roster interface { Actor(context.Context, string) (Actor, error) } // BoxEvidence loads the exact bytes stored at boxid.Key(box). type BoxEvidence interface { Load(context.Context, string) ([]byte, error) } type IdentityProvider struct { HomeRegion string AWSRegion string Roster Roster Evidence BoxEvidence Account, Box, Region, Bucket, Namespace string Certs map[string]*x509.Certificate } func (p IdentityProvider) Verify(ctx context.Context) (Actor, error) { if p.Roster == nil || p.Evidence == nil || p.Account == "" || p.Box == "" || p.Region == "" || p.AWSRegion == "" || p.Bucket == "" || p.Namespace == "" || len(p.Certs) == 0 { return Actor{}, ErrUnsupported } a, err := p.Roster.Actor(ctx, p.Box) if err != nil { return Actor{}, err } if !p.actorMatches(a) { return Actor{}, ErrChanged } raw, err := p.Evidence.Load(ctx, p.Box) if err != nil { return Actor{}, err } // The roster entry is the expected binding: Enroll refuses a record whose // signed instance is not the one this reference is enrolled as. cloud := boxid.Cloud{Provider: a.Cloud.Provider, Account: a.Cloud.Account, Region: a.Cloud.Region, InstanceID: a.Cloud.InstanceID} ev, err := (boxid.Verifier{Account: p.Account, Certs: p.Certs}).Enroll(raw, boxid.Expect{ Box: p.Box, AWSRegion: a.Cloud.Region, Roster: []boxid.Enrolled{{Box: a.Box, Cloud: cloud}}, }) if err != nil { return Actor{}, err } if ev.Box != p.Box || ev.Cloud != cloud { return Actor{}, ErrChanged } if p.HomeRegion != "" && (p.HomeRegion != a.FleetHomeRegion || p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) { return Actor{}, ErrChanged } a.HomeRegion = p.HomeRegion return a, nil } func (p IdentityProvider) actorMatches(a Actor) bool { return p.AWSRegion != "" && a.Box == p.Box && a.Enrollment != "" && a.Revision > 0 && a.Binding != "" && a.Eligible && !a.Quarantined && a.Region == p.Region && a.Bucket == p.Bucket && a.Namespace == p.Namespace && a.Cloud.Provider == "aws" && a.Cloud.Account == p.Account && a.Cloud.Region == p.AWSRegion && a.Cloud.InstanceID != "" } // LocalAuthority is Plan 8's nonblocking local-state reader. It checks its own // boot-clock deadline, loss latch, controller identity, quarantine and fences. // No second execution lease state machine is introduced here. type LocalAuthority interface { Check(context.Context, Actor, *hostclaim.Claim, Selection) error } func (p IdentityProvider) Local(authority LocalAuthority) func(context.Context, *hostclaim.Claim, Selection) error { return func(ctx context.Context, c *hostclaim.Claim, s Selection) error { if authority == nil { return ErrUnsupported } actor, err := p.Verify(ctx) if err != nil { return err } if s.Box != actor.Box || s.Region != actor.Region || s.HomeRegion != actor.HomeRegion || s.FleetHomeRegion != actor.FleetHomeRegion { return ErrChanged } return authority.Check(ctx, actor, c, s) } } // Startup evaluates the switch before constructing any authority provider. func Startup(ctx context.Context, getenv func(string) string, construct func(context.Context) (hostwrite.Selector, error)) (hostwrite.Config, error) { c := hostwrite.Config{Enabled: hostwrite.Enabled(getenv)} if !c.Enabled { return c, nil } if construct == nil { return c, hostwrite.ErrUnavailable } selector, err := construct(ctx) if err != nil { return c, err } if selector == nil { return c, hostwrite.ErrUnavailable } c.Selector = selector return c, nil }