Allow Operator Generated bootstrap token (#12520)

This commit is contained in:
Lance Haig 2022-06-03 13:37:24 +02:00 committed by GitHub
parent 50f890a2da
commit 4bf27d743d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 351 additions and 10 deletions

3
.changelog/12520.txt Normal file
View File

@ -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
.gitignore vendored
View File

@ -77,8 +77,10 @@ GNUMakefile.local
rkt-*
# Common editor config
./idea
*.iml
.vscode
# UI rules

View File

@ -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
@ -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
@ -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
}

View File

@ -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)
}

View File

@ -2,6 +2,8 @@ package command
import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/hashicorp/nomad/api"
@ -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)
@ -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 {
@ -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

View File

@ -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) {
@ -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)
}

View File

@ -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

View File

@ -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) {

View File

@ -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"
@ -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
@ -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

View File

@ -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()

View File

@ -11909,8 +11909,9 @@ type ACLTokenDeleteRequest struct {
// ACLTokenBootstrapRequest is used to bootstrap ACLs
type ACLTokenBootstrapRequest struct {
Token *ACLToken // Not client specifiable
ResetIndex uint64 // Reset index is used to clear the bootstrap token
Token *ACLToken // Not client specifiable
ResetIndex uint64 // Reset index is used to clear the bootstrap token
BootstrapSecret string
WriteRequest
}

View File

@ -11,7 +11,10 @@ For more details about ACLs, please see the [ACL Guide](https://learn.hashicorp.
## Bootstrap Token
This endpoint is used to bootstrap the ACL system and provide the initial management token.
This endpoint is used to bootstrap the ACL system and provide the initial management token.
An operator created token can be provided in the body of the request to bootstrap the cluster
if required. If no header is provided the cluster will return a generated management token.
The provided token should be presented in a UUID format.
This request is always forwarded to the authoritative region. It can only be invoked once
until a [bootstrap reset](https://learn.hashicorp.com/tutorials/nomad/access-control-bootstrap#re-bootstrap-acl-system) is performed.
@ -51,6 +54,39 @@ $ curl \
}
```
### Sample Operator Payload
```json
{
"BootstrapSecret": "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
}
```
### Sample Request With Operator Token
```shell-session
$ curl \
--request POST \
--data @root-token.json \
https://localhost:4646/v1/acl/bootstrap
```
### Sample Response With Operator Token
```json
{
"AccessorID": "b780e702-98ce-521f-2e5f-c6b87de05b24",
"SecretID": "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
"Name": "Bootstrap Token",
"Type": "management",
"Policies": null,
"Global": true,
"CreateTime": "2017-08-23T22:47:14.695408057Z",
"CreateIndex": 7,
"ModifyIndex": 7
}
```
## List Tokens
This endpoint lists all ACL tokens. This lists the local tokens and the global

View File

@ -15,7 +15,11 @@ The `acl bootstrap` command is used to bootstrap the initial ACL token.
nomad acl bootstrap [options]
```
The `acl bootstrap` command requires no arguments.
The `acl bootstrap` command can be used in two ways:
- If you provide no arguments it will return a system generated bootstrap token.
- If you would like to provide an operator generated token it is possible to provide the token
using a file `acl bootstrap [path]`. The Token can be read from stdin by setting the path to "-".
Please make sure you secure this token in an apropriate manner as it could be written to your terminal history.
## General Options
@ -28,7 +32,7 @@ The `acl bootstrap` command requires no arguments.
## Examples
Bootstrap the initial token:
Bootstrap the initial token without arguments:
```shell-session
$ nomad acl bootstrap
@ -42,3 +46,40 @@ Create Time = 2017-09-11 17:38:10.999089612 +0000 UTC
Create Index = 7
Modify Index = 7
```
Bootstrap the initial token with provided token:
root.token file
```shell-session
2b778dd9-f5f1-6f29-b4b4-9a5fa948757a
```
```shell-session
$ nomad acl bootstrap root.token
Accessor ID = 5b7fd453-d3f7-6814-81dc-fcfe6daedea5
Secret ID = 2b778dd9-f5f1-6f29-b4b4-9a5fa948757a
Name = Bootstrap Token
Type = management
Global = true
Policies = n/a
Create Time = 2017-09-11 17:38:10.999089612 +0000 UTC
Create Index = 7
Modify Index = 7
```
Bootstrap the initial token with provided token using stdin
```shell-session
$ nomad acl bootstrap -
2b778dd9-f5f1-6f29-b4b4-9a5fa948757a
EOF
Accessor ID = 5b7fd453-d3f7-6814-81dc-fcfe6daedea5
Secret ID = 2b778dd9-f5f1-6f29-b4b4-9a5fa948757a
Name = Bootstrap Token
Type = management
Global = true
Policies = n/a
Create Time = 2017-09-11 17:38:10.999089612 +0000 UTC
Create Index = 7
Modify Index = 7
```