Allow Operator Generated bootstrap token (#12520)
This commit is contained in:
parent
50f890a2da
commit
4bf27d743d
|
@ -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
|
||||
```
|
|
@ -77,8 +77,10 @@ GNUMakefile.local
|
|||
|
||||
rkt-*
|
||||
|
||||
# Common editor config
|
||||
./idea
|
||||
*.iml
|
||||
.vscode
|
||||
|
||||
# UI rules
|
||||
|
||||
|
|
23
api/acl.go
23
api/acl.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -11911,6 +11911,7 @@ type ACLTokenDeleteRequest struct {
|
|||
type ACLTokenBootstrapRequest struct {
|
||||
Token *ACLToken // Not client specifiable
|
||||
ResetIndex uint64 // Reset index is used to clear the bootstrap token
|
||||
BootstrapSecret string
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ 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.
|
||||
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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue