Skip to content

Commit 6925845

Browse files
committed
Allow Operator Generated bootstrap token
1 parent 095764a commit 6925845

File tree

11 files changed

+293
-12
lines changed

11 files changed

+293
-12
lines changed

.changelog/14437.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
```release-note:improvement
3+
bootstrap: Added option to allow for an operator generated bootstrap token to be passed to the `acl bootstrap` command
4+
```

agent/acl_endpoint.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request)
3434
return nil, aclDisabled
3535
}
3636

37-
args := structs.DCSpecificRequest{
37+
args := structs.ACLInitialTokenBootstrapRequest{
3838
Datacenter: s.agent.config.Datacenter,
3939
}
40+
41+
if req.ContentLength != 0 {
42+
var bootstrapSecretRequest struct {
43+
BootstrapSecret string
44+
}
45+
if err := lib.DecodeJSON(req.Body, &bootstrapSecretRequest); err != nil {
46+
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Request decoding failed: %v", err)}
47+
}
48+
args.BootstrapSecret = bootstrapSecretRequest.BootstrapSecret
49+
}
50+
4051
var out structs.ACLToken
4152
err := s.agent.RPC("ACL.BootstrapTokens", &args, &out)
4253
if err != nil {

agent/acl_endpoint_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,64 @@ func TestACL_Bootstrap(t *testing.T) {
147147
}
148148
}
149149

150+
func TestACL_BootstrapWithToken(t *testing.T) {
151+
if testing.Short() {
152+
t.Skip("too slow for testing.Short")
153+
}
154+
155+
t.Parallel()
156+
a := NewTestAgent(t, `
157+
primary_datacenter = "dc1"
158+
159+
acl {
160+
enabled = true
161+
default_policy = "deny"
162+
}
163+
`)
164+
defer a.Shutdown()
165+
166+
tests := []struct {
167+
name string
168+
method string
169+
code int
170+
token bool
171+
}{
172+
{"bootstrap", "PUT", http.StatusOK, true},
173+
{"not again", "PUT", http.StatusForbidden, false},
174+
}
175+
testrpc.WaitForLeader(t, a.RPC, "dc1")
176+
for _, tt := range tests {
177+
t.Run(tt.name, func(t *testing.T) {
178+
var bootstrapSecret struct {
179+
BootstrapSecret string
180+
}
181+
bootstrapSecret.BootstrapSecret = "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
182+
resp := httptest.NewRecorder()
183+
req, _ := http.NewRequest(tt.method, "/v1/acl/bootstrap", jsonBody(bootstrapSecret))
184+
out, err := a.srv.ACLBootstrap(resp, req)
185+
if tt.token && err != nil {
186+
t.Fatalf("err: %v", err)
187+
}
188+
if tt.token {
189+
wrap, ok := out.(*aclBootstrapResponse)
190+
if !ok {
191+
t.Fatalf("bad: %T", out)
192+
}
193+
if wrap.ID != bootstrapSecret.BootstrapSecret {
194+
t.Fatalf("bad: %v", wrap)
195+
}
196+
if wrap.ID != wrap.SecretID {
197+
t.Fatalf("bad: %v", wrap)
198+
}
199+
} else {
200+
if out != nil {
201+
t.Fatalf("bad: %T", out)
202+
}
203+
}
204+
})
205+
}
206+
}
207+
150208
func TestACL_HTTP(t *testing.T) {
151209
if testing.Short() {
152210
t.Skip("too slow for testing.Short")

agent/consul/acl_endpoint.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (a *ACL) aclPreCheck() error {
164164

165165
// BootstrapTokens is used to perform a one-time ACL bootstrap operation on
166166
// a cluster to get the first management token.
167-
func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.ACLToken) error {
167+
func (a *ACL) BootstrapTokens(args *structs.ACLInitialTokenBootstrapRequest, reply *structs.ACLToken) error {
168168
if err := a.aclPreCheck(); err != nil {
169169
return err
170170
}
@@ -209,9 +209,24 @@ func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.AC
209209
if err != nil {
210210
return err
211211
}
212-
secret, err := lib.GenerateUUID(a.srv.checkTokenUUID)
213-
if err != nil {
214-
return err
212+
secret := args.BootstrapSecret
213+
if secret == "" {
214+
secret, err = lib.GenerateUUID(a.srv.checkTokenUUID)
215+
if err != nil {
216+
return err
217+
}
218+
} else {
219+
_, err = uuid.ParseUUID(secret)
220+
if err != nil {
221+
return err
222+
}
223+
ok, err := a.srv.checkTokenUUID(secret)
224+
if err != nil {
225+
return err
226+
}
227+
if !ok {
228+
return fmt.Errorf("Provided token cannot be used")
229+
}
215230
}
216231

217232
req := structs.ACLTokenBootstrapRequest{

agent/consul/acl_endpoint_test.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func TestACLEndpoint_BootstrapTokens(t *testing.T) {
3939
waitForLeaderEstablishment(t, srv)
4040

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

76+
func TestACLEndpoint_ProvidedBootstrapTokens(t *testing.T) {
77+
if testing.Short() {
78+
t.Skip("too slow for testing.Short")
79+
}
80+
81+
t.Parallel()
82+
_, srv, codec := testACLServerWithConfig(t, func(c *Config) {
83+
// remove this as we are bootstrapping
84+
c.ACLInitialManagementToken = ""
85+
}, false)
86+
waitForLeaderEstablishment(t, srv)
87+
88+
// Expect an error initially since ACL bootstrap is not initialized.
89+
arg := structs.ACLInitialTokenBootstrapRequest{
90+
Datacenter: "dc1",
91+
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
92+
}
93+
var out structs.ACLToken
94+
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.BootstrapTokens", &arg, &out))
95+
require.Equal(t, out.SecretID, arg.BootstrapSecret)
96+
require.Equal(t, 36, len(out.AccessorID))
97+
require.True(t, strings.HasPrefix(out.Description, "Bootstrap Token"))
98+
require.True(t, out.CreateIndex > 0)
99+
require.Equal(t, out.CreateIndex, out.ModifyIndex)
100+
}
101+
102+
func TestACLEndpoint_ProvidedBootstrapTokensInvalid(t *testing.T) {
103+
if testing.Short() {
104+
t.Skip("too slow for testing.Short")
105+
}
106+
107+
t.Parallel()
108+
_, srv, codec := testACLServerWithConfig(t, func(c *Config) {
109+
// remove this as we are bootstrapping
110+
c.ACLInitialManagementToken = ""
111+
}, false)
112+
waitForLeaderEstablishment(t, srv)
113+
114+
// Expect an error initially since ACL bootstrap is not initialized.
115+
arg := structs.ACLInitialTokenBootstrapRequest{
116+
Datacenter: "dc1",
117+
BootstrapSecret: "abc",
118+
}
119+
var out structs.ACLToken
120+
require.EqualError(t, msgpackrpc.CallWithCodec(codec, "ACL.BootstrapTokens", &arg, &out), "uuid string is wrong length")
121+
}
122+
76123
func TestACLEndpoint_ReplicationStatus(t *testing.T) {
77124
if testing.Short() {
78125
t.Skip("too slow for testing.Short")

agent/structs/acl.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -1350,10 +1350,20 @@ type ACLTokenBatchDeleteRequest struct {
13501350
TokenIDs []string // Tokens to delete
13511351
}
13521352

1353+
type ACLInitialTokenBootstrapRequest struct {
1354+
BootstrapSecret string
1355+
Datacenter string
1356+
QueryOptions
1357+
}
1358+
1359+
func (r *ACLInitialTokenBootstrapRequest) RequestDatacenter() string {
1360+
return r.Datacenter
1361+
}
1362+
13531363
// ACLTokenBootstrapRequest is used only at the Raft layer
13541364
// for ACL bootstrapping
13551365
//
1356-
// The RPC layer will use a generic DCSpecificRequest to indicate
1366+
// The RPC layer will use ACLInitialTokenBootstrapRequest to indicate
13571367
// that bootstrapping must be performed but the actual token
13581368
// and the resetIndex will be generated by that RPC endpoint
13591369
type ACLTokenBootstrapRequest struct {

api/acl.go

+27
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,11 @@ func (c *Client) ACL() *ACL {
499499
return &ACL{c}
500500
}
501501

502+
// BootstrapRequest is used for when operators provide an ACL Bootstrap Token
503+
type BootstrapRequest struct {
504+
BootstrapSecret string
505+
}
506+
502507
// Bootstrap is used to perform a one-time ACL bootstrap operation on a cluster
503508
// to get the first management token.
504509
func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) {
@@ -519,6 +524,28 @@ func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) {
519524
return &out, wm, nil
520525
}
521526

527+
// BootstrapOpts is used to get the initial bootstrap token or pass in the one that was provided in the API
528+
func (a *ACL) BootstrapOpts(btoken string) (*ACLToken, *WriteMeta, error) {
529+
r := a.c.newRequest("PUT", "/v1/acl/bootstrap")
530+
r.obj = &BootstrapRequest{
531+
BootstrapSecret: btoken,
532+
}
533+
rtt, resp, err := a.c.doRequest(r)
534+
if err != nil {
535+
return nil, nil, err
536+
}
537+
defer closeResponseBody(resp)
538+
if err := requireOK(resp); err != nil {
539+
return nil, nil, err
540+
}
541+
wm := &WriteMeta{RequestTime: rtt}
542+
var out ACLToken
543+
if err := decodeBody(resp, &out); err != nil {
544+
return nil, nil, err
545+
}
546+
return &out, wm, nil
547+
}
548+
522549
// Create is used to generate a new token with the given parameters
523550
//
524551
// Deprecated: Use TokenCreate instead.

command/acl/bootstrap/bootstrap.go

+37-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package bootstrap
33
import (
44
"flag"
55
"fmt"
6+
"io/ioutil"
7+
"os"
68
"strings"
79

10+
"github.com/hashicorp/consul/api"
811
"github.com/hashicorp/consul/command/acl/token"
912
"github.com/hashicorp/consul/command/flags"
1013
"github.com/mitchellh/cli"
@@ -43,13 +46,46 @@ func (c *cmd) Run(args []string) int {
4346
return 1
4447
}
4548

49+
args = c.flags.Args()
50+
if l := len(args); l < 0 || l > 1 {
51+
c.UI.Error("This command takes up to one argument")
52+
return 1
53+
}
54+
55+
var terminalToken []byte
56+
var err error
57+
58+
if len(args) == 1 {
59+
switch args[0] {
60+
case "":
61+
terminalToken = []byte{}
62+
case "-":
63+
terminalToken, err = ioutil.ReadAll(os.Stdin)
64+
default:
65+
file := args[0]
66+
terminalToken, err = ioutil.ReadFile(file)
67+
}
68+
if err != nil {
69+
c.UI.Error(fmt.Sprintf("Error reading provided token: %v", err))
70+
return 1
71+
}
72+
}
73+
74+
// Remove newline from the token if it was passed by stdin
75+
boottoken := strings.TrimSuffix(string(terminalToken), "\n")
76+
4677
client, err := c.http.APIClient()
4778
if err != nil {
4879
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
4980
return 1
5081
}
5182

52-
t, _, err := client.ACL().Bootstrap()
83+
var t *api.ACLToken
84+
if len(boottoken) > 0 {
85+
t, _, err = client.ACL().BootstrapOpts(boottoken)
86+
} else {
87+
t, _, err = client.ACL().Bootstrap()
88+
}
5389
if err != nil {
5490
c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err))
5591
return 1

command/acl/bootstrap/bootstrap_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package bootstrap
22

33
import (
44
"encoding/json"
5+
"io/ioutil"
6+
"os"
57
"strings"
68
"testing"
79

@@ -87,3 +89,50 @@ func TestBootstrapCommand_JSON(t *testing.T) {
8789
err := json.Unmarshal([]byte(output), &jsonOutput)
8890
require.NoError(t, err, "token unmarshalling error")
8991
}
92+
93+
func TestBootstrapCommand_Initial(t *testing.T) {
94+
if testing.Short() {
95+
t.Skip("too slow for testing.Short")
96+
}
97+
98+
t.Parallel()
99+
100+
a := agent.NewTestAgent(t, `
101+
primary_datacenter = "dc1"
102+
acl {
103+
enabled = true
104+
}`)
105+
106+
defer a.Shutdown()
107+
testrpc.WaitForLeader(t, a.RPC, "dc1")
108+
109+
ui := cli.NewMockUi()
110+
cmd := New(ui)
111+
112+
// Create temp file
113+
f, err := ioutil.TempFile("", "consul-token.token")
114+
assert.Nil(t, err)
115+
defer os.Remove(f.Name())
116+
117+
// Write the token to the file
118+
err = ioutil.WriteFile(f.Name(), []byte("2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"), 0700)
119+
assert.Nil(t, err)
120+
121+
args := []string{
122+
"-http-addr=" + a.HTTPAddr(),
123+
"-format=json",
124+
f.Name(),
125+
}
126+
127+
code := cmd.Run(args)
128+
assert.Equal(t, code, 0)
129+
assert.Empty(t, ui.ErrorWriter.String())
130+
output := ui.OutputWriter.String()
131+
assert.Contains(t, output, "Bootstrap Token")
132+
assert.Contains(t, output, structs.ACLPolicyGlobalManagementID)
133+
assert.Contains(t, output, "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a")
134+
135+
var jsonOutput json.RawMessage
136+
err = json.Unmarshal([]byte(output), &jsonOutput)
137+
require.NoError(t, err, "token unmarshalling error")
138+
}

0 commit comments

Comments
 (0)