The token carries the node's name
Found by raising a mesh end to end. The broker account a joining node authenticates as is named after the node, and exists before that machine has been told anything — so the node has to know its name before the mesh can tell it. Without it, enrolment fails at the broker with an empty username, which says nothing about why. Not a secret, and the issuer already knows it. The wire-format test now covers it, so a rename on either side fails in both repositories rather than at enrolment on a real machine.
This commit is contained in:
@@ -352,7 +352,7 @@ func tokenCommand(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
made := token.Token{Signer: key.Public, Secret: issued.Secret}
|
||||
made := token.Token{Node: issued.Node.Name, Signer: key.Public, Secret: issued.Secret}
|
||||
|
||||
// Absent is a state, not a failure: a control plane can hold records and a key before it has
|
||||
// a broker. What it cannot do is issue a token anybody could use, and Missing() says so.
|
||||
|
||||
+15
-1
@@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Token is the four things, and it is assembled by whatever holds all four.
|
||||
// Token is everything a joining node needs, assembled by whatever holds all of it.
|
||||
//
|
||||
// Two contexts contribute: `inventory` owns the node record and mints the secret, `identity` owns
|
||||
// the signing key. Neither reads the other's store — the process holding both grants asks each
|
||||
@@ -27,6 +27,17 @@ import (
|
||||
type Token struct {
|
||||
Version int `json:"v"`
|
||||
|
||||
// Node is what the mesh calls this machine.
|
||||
//
|
||||
// Not a secret and not the node's to choose — the record was created before the token was
|
||||
// issued, and the broker account the node must authenticate as is named after it. So the node
|
||||
// has to know it *before* the mesh can tell it anything, which is why it travels here.
|
||||
//
|
||||
// It was not here at first, and enrolment then needed a separate flag while its own help said
|
||||
// the token was the only thing required. The failure that produced was a connection refused
|
||||
// with an empty username, which says nothing about the cause.
|
||||
Node string `json:"node,omitempty"`
|
||||
|
||||
// Broker is an address and not a name. There is no resolution before joining, which is why
|
||||
// this is the one place in the mesh where an address is carried deliberately.
|
||||
Broker string `json:"broker,omitempty"`
|
||||
@@ -50,6 +61,9 @@ type Token struct {
|
||||
// a compromised broker could then forge declarations, and that is the whole machine.
|
||||
func (t Token) Missing() []string {
|
||||
var missing []string
|
||||
if strings.TrimSpace(t.Node) == "" {
|
||||
missing = append(missing, "the node's name — the broker account is named after it")
|
||||
}
|
||||
if strings.TrimSpace(t.Broker) == "" {
|
||||
missing = append(missing, "the broker's address — there is nowhere to connect to")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func complete(t *testing.T) Token {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Token{
|
||||
Node: "anchor",
|
||||
Broker: "192.0.2.10:5671",
|
||||
Fingerprint: "sha256:" + strings.Repeat("ab", 32),
|
||||
Signer: public,
|
||||
@@ -85,8 +86,8 @@ func TestEveryMissingPartIsNamed(t *testing.T) {
|
||||
// decision into four.
|
||||
empty := Token{}
|
||||
missing := empty.Missing()
|
||||
if len(missing) != 4 {
|
||||
t.Fatalf("an empty token named %d missing parts, expected 4: %v", len(missing), missing)
|
||||
if len(missing) != 5 {
|
||||
t.Fatalf("an empty token named %d missing parts, expected 5: %v", len(missing), missing)
|
||||
}
|
||||
if empty.Complete() {
|
||||
t.Error("an empty token reported itself complete")
|
||||
@@ -105,7 +106,7 @@ func TestAShortSigningKeyIsNotASigningKey(t *testing.T) {
|
||||
|
||||
func TestACompleteTokenIsComplete(t *testing.T) {
|
||||
if got := complete(t); !got.Complete() {
|
||||
t.Errorf("a token with all four parts reported missing: %v", got.Missing())
|
||||
t.Errorf("a token with every part reported missing: %v", got.Missing())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,12 +132,27 @@ func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"v", "broker", "fingerprint", "signer", "secret"} {
|
||||
for _, want := range []string{"v", "node", "broker", "fingerprint", "signer", "secret"} {
|
||||
if _, ok := fields[want]; !ok {
|
||||
t.Errorf("the token has no %q field; the host reads that name", want)
|
||||
}
|
||||
}
|
||||
if len(fields) != 5 {
|
||||
t.Errorf("the token has %d fields, expected 5: %v", len(fields), fields)
|
||||
if len(fields) != 6 {
|
||||
t.Errorf("the token has %d fields, expected 6: %v", len(fields), fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestATokenWithNoNameIsRefused(t *testing.T) {
|
||||
// The broker account a joining node authenticates as is named after the node, so the node has
|
||||
// to know its name before the mesh can tell it anything. Without this the connection is
|
||||
// refused with an empty username, which says nothing about the cause — which is exactly how
|
||||
// it went the first time a mesh was raised end to end.
|
||||
without := complete(t)
|
||||
without.Node = ""
|
||||
if without.Complete() {
|
||||
t.Fatal("a token with no node name reported itself usable")
|
||||
}
|
||||
if !strings.Contains(strings.Join(without.Missing(), " "), "node's name") {
|
||||
t.Fatalf("the refusal does not say what is missing: %v", without.Missing())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user