Skip to content

Commit

Permalink
Allow Operator Generated bootstrap token
Browse files Browse the repository at this point in the history
  • Loading branch information
apollo13 committed Sep 1, 2022
1 parent edbf845 commit 022f47f
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .changelog/14437.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

```release-note:improvement
bootstrap: Added option to allow for an operator-generated bootstrap token to be passed to the `acl bootstrap` command.
```
13 changes: 12 additions & 1 deletion agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request)
return nil, aclDisabled
}

args := structs.DCSpecificRequest{
args := structs.ACLInitialTokenBootstrapRequest{
Datacenter: s.agent.config.Datacenter,
}

if req.ContentLength != 0 {
var bootstrapSecretRequest struct {
BootstrapSecret string
}
if err := lib.DecodeJSON(req.Body, &bootstrapSecretRequest); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Request decoding failed: %v", err)}
}
args.BootstrapSecret = bootstrapSecretRequest.BootstrapSecret
}

var out structs.ACLToken
err := s.agent.RPC("ACL.BootstrapTokens", &args, &out)
if err != nil {
Expand Down
58 changes: 58 additions & 0 deletions agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,64 @@ func TestACL_Bootstrap(t *testing.T) {
}
}

func TestACL_BootstrapWithToken(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()
a := NewTestAgent(t, `
primary_datacenter = "dc1"
acl {
enabled = true
default_policy = "deny"
}
`)
defer a.Shutdown()

tests := []struct {
name string
method string
code int
token bool
}{
{"bootstrap", "PUT", http.StatusOK, true},
{"not again", "PUT", http.StatusForbidden, false},
}
testrpc.WaitForLeader(t, a.RPC, "dc1")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var bootstrapSecret struct {
BootstrapSecret string
}
bootstrapSecret.BootstrapSecret = "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
resp := httptest.NewRecorder()
req, _ := http.NewRequest(tt.method, "/v1/acl/bootstrap", jsonBody(bootstrapSecret))
out, err := a.srv.ACLBootstrap(resp, req)
if tt.token && err != nil {
t.Fatalf("err: %v", err)
}
if tt.token {
wrap, ok := out.(*aclBootstrapResponse)
if !ok {
t.Fatalf("bad: %T", out)
}
if wrap.ID != bootstrapSecret.BootstrapSecret {
t.Fatalf("bad: %v", wrap)
}
if wrap.ID != wrap.SecretID {
t.Fatalf("bad: %v", wrap)
}
} else {
if out != nil {
t.Fatalf("bad: %T", out)
}
}
})
}
}

func TestACL_HTTP(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
Expand Down
23 changes: 19 additions & 4 deletions agent/consul/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (a *ACL) aclPreCheck() error {

// BootstrapTokens is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.ACLToken) error {
func (a *ACL) BootstrapTokens(args *structs.ACLInitialTokenBootstrapRequest, reply *structs.ACLToken) error {
if err := a.aclPreCheck(); err != nil {
return err
}
Expand Down Expand Up @@ -209,9 +209,24 @@ func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.AC
if err != nil {
return err
}
secret, err := lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
secret := args.BootstrapSecret
if secret == "" {
secret, err = lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
} else {
_, err = uuid.ParseUUID(secret)
if err != nil {
return err
}
ok, err := a.srv.checkTokenUUID(secret)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("Provided token cannot be used")
}
}

req := structs.ACLTokenBootstrapRequest{
Expand Down
49 changes: 48 additions & 1 deletion agent/consul/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestACLEndpoint_BootstrapTokens(t *testing.T) {
waitForLeaderEstablishment(t, srv)

// Expect an error initially since ACL bootstrap is not initialized.
arg := structs.DCSpecificRequest{
arg := structs.ACLInitialTokenBootstrapRequest{
Datacenter: "dc1",
}
var out structs.ACLToken
Expand Down Expand Up @@ -73,6 +73,53 @@ func TestACLEndpoint_BootstrapTokens(t *testing.T) {
require.Equal(t, out.CreateIndex, out.ModifyIndex)
}

func TestACLEndpoint_ProvidedBootstrapTokens(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()
_, srv, codec := testACLServerWithConfig(t, func(c *Config) {
// remove this as we are bootstrapping
c.ACLInitialManagementToken = ""
}, false)
waitForLeaderEstablishment(t, srv)

// Expect an error initially since ACL bootstrap is not initialized.
arg := structs.ACLInitialTokenBootstrapRequest{
Datacenter: "dc1",
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
}
var out structs.ACLToken
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.BootstrapTokens", &arg, &out))
require.Equal(t, out.SecretID, arg.BootstrapSecret)
require.Equal(t, 36, len(out.AccessorID))
require.True(t, strings.HasPrefix(out.Description, "Bootstrap Token"))
require.True(t, out.CreateIndex > 0)
require.Equal(t, out.CreateIndex, out.ModifyIndex)
}

func TestACLEndpoint_ProvidedBootstrapTokensInvalid(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()
_, srv, codec := testACLServerWithConfig(t, func(c *Config) {
// remove this as we are bootstrapping
c.ACLInitialManagementToken = ""
}, false)
waitForLeaderEstablishment(t, srv)

// Expect an error initially since ACL bootstrap is not initialized.
arg := structs.ACLInitialTokenBootstrapRequest{
Datacenter: "dc1",
BootstrapSecret: "abc",
}
var out structs.ACLToken
require.EqualError(t, msgpackrpc.CallWithCodec(codec, "ACL.BootstrapTokens", &arg, &out), "uuid string is wrong length")
}

func TestACLEndpoint_ReplicationStatus(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
Expand Down
12 changes: 11 additions & 1 deletion agent/structs/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1350,10 +1350,20 @@ type ACLTokenBatchDeleteRequest struct {
TokenIDs []string // Tokens to delete
}

type ACLInitialTokenBootstrapRequest struct {
BootstrapSecret string
Datacenter string
QueryOptions
}

func (r *ACLInitialTokenBootstrapRequest) RequestDatacenter() string {
return r.Datacenter
}

// ACLTokenBootstrapRequest is used only at the Raft layer
// for ACL bootstrapping
//
// The RPC layer will use a generic DCSpecificRequest to indicate
// The RPC layer will use ACLInitialTokenBootstrapRequest to indicate
// that bootstrapping must be performed but the actual token
// and the resetIndex will be generated by that RPC endpoint
type ACLTokenBootstrapRequest struct {
Expand Down
27 changes: 27 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,11 @@ func (c *Client) ACL() *ACL {
return &ACL{c}
}

// BootstrapRequest is used for when operators provide an ACL Bootstrap Token
type BootstrapRequest struct {
BootstrapSecret string
}

// Bootstrap is used to perform a one-time ACL bootstrap operation on a cluster
// to get the first management token.
func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) {
Expand All @@ -519,6 +524,28 @@ func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) {
return &out, wm, nil
}

// BootstrapOpts is used to get the initial bootstrap token or pass in the one that was provided in the API
func (a *ACL) BootstrapOpts(btoken string) (*ACLToken, *WriteMeta, error) {
r := a.c.newRequest("PUT", "/v1/acl/bootstrap")
r.obj = &BootstrapRequest{
BootstrapSecret: btoken,
}
rtt, resp, err := a.c.doRequest(r)
if err != nil {
return nil, nil, err
}
defer closeResponseBody(resp)
if err := requireOK(resp); err != nil {
return nil, nil, err
}
wm := &WriteMeta{RequestTime: rtt}
var out ACLToken
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}

// Create is used to generate a new token with the given parameters
//
// Deprecated: Use TokenCreate instead.
Expand Down
38 changes: 37 additions & 1 deletion command/acl/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package bootstrap
import (
"flag"
"fmt"
"io/ioutil"
"os"
"strings"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl/token"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
Expand Down Expand Up @@ -43,13 +46,46 @@ func (c *cmd) Run(args []string) int {
return 1
}

args = c.flags.Args()
if l := len(args); l < 0 || l > 1 {
c.UI.Error("This command takes up to one argument")
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")

client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}

t, _, err := client.ACL().Bootstrap()
var t *api.ACLToken
if len(boottoken) > 0 {
t, _, err = client.ACL().BootstrapOpts(boottoken)
} else {
t, _, err = client.ACL().Bootstrap()
}
if err != nil {
c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err))
return 1
Expand Down
49 changes: 49 additions & 0 deletions command/acl/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package bootstrap

import (
"encoding/json"
"io/ioutil"
"os"
"strings"
"testing"

Expand Down Expand Up @@ -87,3 +89,50 @@ func TestBootstrapCommand_JSON(t *testing.T) {
err := json.Unmarshal([]byte(output), &jsonOutput)
require.NoError(t, err, "token unmarshalling error")
}

func TestBootstrapCommand_Initial(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()

a := agent.NewTestAgent(t, `
primary_datacenter = "dc1"
acl {
enabled = true
}`)

defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")

ui := cli.NewMockUi()
cmd := New(ui)

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

// Write the token to the file
err = ioutil.WriteFile(f.Name(), []byte("2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"), 0700)
assert.Nil(t, err)

args := []string{
"-http-addr=" + a.HTTPAddr(),
"-format=json",
f.Name(),
}

code := cmd.Run(args)
assert.Equal(t, code, 0)
assert.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
assert.Contains(t, output, "Bootstrap Token")
assert.Contains(t, output, structs.ACLPolicyGlobalManagementID)
assert.Contains(t, output, "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a")

var jsonOutput json.RawMessage
err = json.Unmarshal([]byte(output), &jsonOutput)
require.NoError(t, err, "token unmarshalling error")
}
Loading

0 comments on commit 022f47f

Please sign in to comment.