Skip to content

Commit

Permalink
Allow Operator Generated bootstrap token (#12520)
Browse files Browse the repository at this point in the history
  • Loading branch information
lhaig authored and ChaiWithJai committed Jun 3, 2022
1 parent a973594 commit 7e6f107
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .changelog/12520.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
bootstrap: Added option to allow for an operator generated bootstrap token to be passed to the `acl bootstrap` command
```
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ GNUMakefile.local

rkt-*

# Common editor config
./idea
*.iml
.vscode

# UI rules

Expand Down
23 changes: 23 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (c *Client) ACLTokens() *ACLTokens {
return &ACLTokens{client: c}
}

// DEPRECATED: will be removed in Nomad 1.5.0
// Bootstrap is used to get the initial bootstrap token
func (a *ACLTokens) Bootstrap(q *WriteOptions) (*ACLToken, *WriteMeta, error) {
var resp ACLToken
Expand All @@ -82,6 +83,23 @@ func (a *ACLTokens) Bootstrap(q *WriteOptions) (*ACLToken, *WriteMeta, error) {
return &resp, wm, nil
}

// BootstrapOpts is used to get the initial bootstrap token or pass in the one that was provided in the API
func (a *ACLTokens) BootstrapOpts(btoken string, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
if q == nil {
q = &WriteOptions{}
}
req := &BootstrapRequest{
BootstrapSecret: btoken,
}

var resp ACLToken
wm, err := a.client.write("/v1/acl/bootstrap", req, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// List is used to dump all of the tokens.
func (a *ACLTokens) List(q *QueryOptions) ([]*ACLTokenListStub, *QueryMeta, error) {
var resp []*ACLTokenListStub
Expand Down Expand Up @@ -244,3 +262,8 @@ type OneTimeTokenExchangeRequest struct {
type OneTimeTokenExchangeResponse struct {
Token *ACLToken
}

// BootstrapRequest is used for when operators provide an ACL Bootstrap Token
type BootstrapRequest struct {
BootstrapSecret string
}
30 changes: 30 additions & 0 deletions api/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,33 @@ func TestACL_OneTimeToken(t *testing.T) {
assert.NotNil(t, out3)
assert.Equal(t, out3.AccessorID, out.AccessorID)
}

func TestACLTokens_BootstrapInvalidToken(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
})
defer s.Stop()
at := c.ACLTokens()

bootkn := "badtoken"
// Bootstrap with invalid token
_, _, err := at.BootstrapOpts(bootkn, nil)
assert.EqualError(t, err, "Unexpected response code: 400 (invalid acl token)")
}

func TestACLTokens_BootstrapValidToken(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
})
defer s.Stop()
at := c.ACLTokens()

bootkn := "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
// Bootstrap with Valid token
out, wm, err := at.BootstrapOpts(bootkn, nil)
assert.NoError(t, err)
assertWriteMeta(t, wm)
assert.Equal(t, bootkn, out.SecretID)
}
31 changes: 28 additions & 3 deletions command/acl_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package command

import (
"fmt"
"io/ioutil"
"os"
"strings"

"github.com/hashicorp/nomad/api"
Expand Down Expand Up @@ -57,6 +59,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
var (
json bool
tmpl string
file string
)

flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
Expand All @@ -69,12 +72,34 @@ func (c *ACLBootstrapCommand) Run(args []string) int {

// Check that we got no arguments
args = flags.Args()
if l := len(args); l != 0 {
c.Ui.Error("This command takes no arguments")
if l := len(args); l < 0 || l > 1 {
c.Ui.Error("This command takes up to one argument")
c.Ui.Error(commandErrorText(c))
return 1
}

var terminalToken []byte
var err error

if len(args) == 1 {
switch args[0] {
case "":
terminalToken = []byte{}
case "-":
terminalToken, err = ioutil.ReadAll(os.Stdin)
default:
file = args[0]
terminalToken, err = ioutil.ReadFile(file)
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading provided token: %v", err))
return 1
}
}

// Remove newline from the token if it was passed by stdin
boottoken := strings.TrimSuffix(string(terminalToken), "\n")

// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
Expand All @@ -83,7 +108,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
}

// Get the bootstrap token
token, _, err := client.ACLTokens().Bootstrap(nil)
token, _, err := client.ACLTokens().BootstrapOpts(boottoken, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error bootstrapping: %s", err))
return 1
Expand Down
79 changes: 79 additions & 0 deletions command/acl_bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package command

import (
"io/ioutil"
"os"
"testing"

"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestACLBootstrapCommand(t *testing.T) {
Expand Down Expand Up @@ -76,3 +80,78 @@ func TestACLBootstrapCommand_NonACLServer(t *testing.T) {
out := ui.OutputWriter.String()
assert.NotContains(out, "Secret ID")
}

// Attempting to bootstrap the server with an operator provided token in a file should
// return the same token in the result.
func TestACLBootstrapCommand_WithOperatorFileBootstrapToken(t *testing.T) {
ci.Parallel(t)
// create a acl-enabled server without bootstrapping the token
config := func(c *agent.Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0
}

// create a valid token
mockToken := mock.ACLToken()

// Create temp file
f, err := ioutil.TempFile("", "nomad-token.token")
assert.Nil(t, err)
defer os.Remove(f.Name())

// Write the token to the file
err = ioutil.WriteFile(f.Name(), []byte(mockToken.SecretID), 0700)
assert.Nil(t, err)

srv, _, url := testServer(t, true, config)
defer srv.Shutdown()

require.Nil(t, srv.RootToken)

ui := cli.NewMockUi()
cmd := &ACLBootstrapCommand{Meta: Meta{Ui: ui, flagAddress: url}}

code := cmd.Run([]string{"-address=" + url, f.Name()})
assert.Equal(t, 0, code)

out := ui.OutputWriter.String()
assert.Contains(t, out, mockToken.SecretID)
}

// Attempting to bootstrap the server with an invalid operator provided token in a file should
// fail.
func TestACLBootstrapCommand_WithBadOperatorFileBootstrapToken(t *testing.T) {
ci.Parallel(t)

// create a acl-enabled server without bootstrapping the token
config := func(c *agent.Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0
}

// create a invalid token
invalidToken := "invalid-token"

// Create temp file
f, err := ioutil.TempFile("", "nomad-token.token")
assert.Nil(t, err)
defer os.Remove(f.Name())

// Write the token to the file
err = ioutil.WriteFile(f.Name(), []byte(invalidToken), 0700)
assert.Nil(t, err)

srv, _, url := testServer(t, true, config)
defer srv.Shutdown()

assert.Nil(t, srv.RootToken)

ui := cli.NewMockUi()
cmd := &ACLBootstrapCommand{Meta: Meta{Ui: ui, flagAddress: url}}

code := cmd.Run([]string{"-address=" + url, f.Name()})
assert.Equal(t, 1, code)

out := ui.OutputWriter.String()
assert.NotContains(t, out, invalidToken)
}
10 changes: 8 additions & 2 deletions command/agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,14 @@ func (s *HTTPServer) ACLTokenBootstrap(resp http.ResponseWriter, req *http.Reque
return nil, CodedError(405, ErrInvalidMethod)
}

// Format the request
args := structs.ACLTokenBootstrapRequest{}
var args structs.ACLTokenBootstrapRequest

if req.ContentLength != 0 {
if err := decodeBody(req, &args); err != nil {
return nil, CodedError(400, err.Error())
}
}

s.parseWriteRequest(req, &args.WriteRequest)

var out structs.ACLTokenUpsertResponse
Expand Down
43 changes: 43 additions & 0 deletions command/agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,49 @@ func TestHTTP_ACLTokenBootstrap(t *testing.T) {
})
}

func TestHTTP_ACLTokenBootstrapOperator(t *testing.T) {
ci.Parallel(t)
conf := func(c *Config) {
c.ACL.Enabled = true
c.ACL.PolicyTTL = 0 // Special flag to disable auto-bootstrap
}
httpTest(t, conf, func(s *TestAgent) {
// Provide token
args := structs.ACLTokenBootstrapRequest{
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
}

buf := encodeReq(args)

// Make the HTTP request
req, err := http.NewRequest("PUT", "/v1/acl/bootstrap", buf)
if err != nil {
t.Fatalf("err: %v", err)
}

// Since we're not actually writing this HTTP request, we have
// to manually set ContentLength
req.ContentLength = -1

respW := httptest.NewRecorder()
// Make the request
obj, err := s.Server.ACLTokenBootstrap(respW, req)
if err != nil {
t.Fatalf("err: %v", err)
}

// Check for the index
if respW.Result().Header.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}

// Check the output
n := obj.(*structs.ACLToken)
assert.NotNil(t, n)
assert.Equal(t, args.BootstrapSecret, n.SecretID)
})
}

func TestHTTP_ACLTokenList(t *testing.T) {
ci.Parallel(t)
httpACLTest(t, nil, func(s *TestAgent) {
Expand Down
12 changes: 12 additions & 0 deletions nomad/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
log "github.com/hashicorp/go-hclog"
memdb "github.com/hashicorp/go-memdb"
policy "github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/state/paginator"
Expand Down Expand Up @@ -353,6 +354,7 @@ func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.A
return aclDisabled
}
args.Region = a.srv.config.AuthoritativeRegion
providedTokenID := args.BootstrapSecret

if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done {
return err
Expand Down Expand Up @@ -396,6 +398,16 @@ func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.A
Global: true,
CreateTime: time.Now().UTC(),
}

// if a token has been passed in from the API overwrite the generated one.
if providedTokenID != "" {
if helper.IsUUID(providedTokenID) {
args.Token.SecretID = providedTokenID
} else {
return structs.NewErrRPCCodedf(400, "invalid acl token")
}
}

args.Token.SetHash()

// Update via Raft
Expand Down
40 changes: 40 additions & 0 deletions nomad/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,46 @@ func TestACLEndpoint_Bootstrap(t *testing.T) {
assert.Equal(t, created, out)
}

func TestACLEndpoint_BootstrapOperator(t *testing.T) {
ci.Parallel(t)
s1, cleanupS1 := TestServer(t, func(c *Config) {
c.ACLEnabled = true
})
defer cleanupS1()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)

// Lookup the tokens
req := &structs.ACLTokenBootstrapRequest{
WriteRequest: structs.WriteRequest{Region: "global"},
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
}
var resp structs.ACLTokenUpsertResponse
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil {
t.Fatalf("err: %v", err)
}
assert.NotEqual(t, uint64(0), resp.Index)
assert.NotNil(t, resp.Tokens[0])

// Get the token out from the response
created := resp.Tokens[0]
assert.NotEqual(t, "", created.AccessorID)
assert.NotEqual(t, "", created.SecretID)
assert.NotEqual(t, time.Time{}, created.CreateTime)
assert.Equal(t, structs.ACLManagementToken, created.Type)
assert.Equal(t, "Bootstrap Token", created.Name)
assert.Equal(t, true, created.Global)

// Check we created the token
out, err := s1.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
assert.Nil(t, err)
assert.Equal(t, created, out)
// Check we have the correct operator token
tokenout, err := s1.fsm.State().ACLTokenBySecretID(nil, created.SecretID)
assert.Nil(t, err)
assert.Equal(t, created, tokenout)
}

func TestACLEndpoint_Bootstrap_Reset(t *testing.T) {
ci.Parallel(t)
dir := t.TempDir()
Expand Down
Loading

0 comments on commit 7e6f107

Please sign in to comment.