Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Operator Generated bootstrap token #14437

Merged
merged 2 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/14437.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
acl: 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 @@ -7,6 +7,7 @@ import (

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
)

Expand Down Expand Up @@ -34,9 +35,19 @@ func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request)
return nil, aclDisabled
}

args := structs.DCSpecificRequest{
args := structs.ACLInitialTokenBootstrapRequest{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of anyone else reading this PR, note that here we are switching the request type completely from one struct to another, and that is being encoded using msgpack and sent over RPC.

The new struct type ACLInitialTokenBootstrapRequest only contains a subset of the fields that exist on DCSpecificRequest so a surface read would say that this is backwards incompatible by msgpack encoding rules and would be guaranteed to lose data if you were using a different version of consul on both sides of this RPC (as you would during windows of time during an upgrade).

A deeper read shows that actually this generic request struct is used in many places, but is largely unused within the acl bootstrap codepath and all of the relevant fields were already copied to your new type with the same name+types, so it would be effectively backwards compatible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a line of code that a future dev might look at and go "wait, this looks like it might be wrong..."? If so, we might want an in-source comment. I defer to @rboyer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in-source might not be correct because it only matters for version-straddling. If it would go anywhere I'd say it would make sense in the body of the git commit so it's historically annotating the change as "yep, this was fine from a msgpack perspective at the time we did it".

Datacenter: s.agent.config.Datacenter,
}

// Handle optional request body
if req.ContentLength > 0 {
var bootstrapSecretRequest api.BootstrapRequest
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(req.Context(), "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 @@ -148,6 +148,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 @@ -162,7 +162,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 @@ -207,9 +207,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a comment. On FIRST bootstrap this doesn't matter because there are no tokens. On rebootstrap however there may already be tokens and you don't want to collide with them. If we didn't allow re-bootstrap this function call would be unnecessary.

Copy link
Contributor Author

@apollo13 apollo13 Nov 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkTokenUUID also checks that the token does not start with ACLReservedPrefix, I'd like to at least keep that check. Should I switch to using ACLIDReserved(secret)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to leave it as is because it is important to check against the existing database during the reset procedure: https://developer.hashicorp.com/consul/tutorials/security/access-control-troubleshoot#reset-the-acl-system

if err != nil {
return err
}
if !ok {
return fmt.Errorf("Provided token cannot be used because a token with that secret already exists.")
}
}

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 @@ -38,7 +38,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 @@ -72,6 +72,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 @@ -1351,10 +1351,20 @@ type ACLTokenBatchDeleteRequest struct {
TokenIDs []string // Tokens to delete
}

type ACLInitialTokenBootstrapRequest struct {
BootstrapSecret string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you feel about calling it InitialToken or InitialManagementToken instead? The config file approach for bootstrap refers to the same concept as initial_management so this would align them.

(whatever happens here it should also be mirrored to the matching api struct)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings; I mirrored nomad: https://github.com/hashicorp/nomad/blob/fbe9f590489e291b3abcfb0fb1548a9f435a5e2d/nomad/structs/structs.go#L12169 -- I'd be fine with changing it if you think another name is better.

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
15 changes: 15 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,25 @@ 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) {
return a.BootstrapWithToken("")
}

// BootstrapWithToken is used to get the initial bootstrap token or pass in the one that was provided in the API
func (a *ACL) BootstrapWithToken(btoken string) (*ACLToken, *WriteMeta, error) {
r := a.c.newRequest("PUT", "/v1/acl/bootstrap")
if btoken != "" {
r.obj = &BootstrapRequest{
BootstrapSecret: btoken,
}
}
rtt, resp, err := a.c.doRequest(r)
if err != nil {
return nil, nil, err
Expand Down
26 changes: 25 additions & 1 deletion command/acl/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package bootstrap
import (
"flag"
"fmt"
"os"
"strings"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl/token"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/mitchellh/cli"
)

Expand Down Expand Up @@ -43,13 +46,34 @@ 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 string
var err error

if len(args) == 1 {
terminalToken, err = helpers.LoadDataSourceNoRaw(args[0], os.Stdin)
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.TrimSpace(terminalToken)

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
t, _, err = client.ACL().BootstrapWithToken(boottoken)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err))
return 1
Expand Down
48 changes: 48 additions & 0 deletions command/acl/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bootstrap

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

Expand Down Expand Up @@ -87,3 +88,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 := os.CreateTemp("", "consul-token.token")
assert.Nil(t, err)
defer os.Remove(f.Name())

// Write the token to the file
err = os.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")
}
21 changes: 19 additions & 2 deletions website/content/api-docs/acl/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ the [ACL tutorial](https://learn.hashicorp.com/tutorials/consul/access-control-s
This endpoint does a special one-time bootstrap of the ACL system, making the first
management token if the [`acl.tokens.initial_management`](/docs/agent/config/config-files#acl_tokens_initial_management)
configuration entry is not specified in the Consul server configuration and if the
cluster has not been bootstrapped previously. This is available in Consul 0.9.1 and later,
and requires all Consul servers to be upgraded in order to operate.
cluster has not been bootstrapped previously. An operator created token can be provided in the body of the request to
bootstrap the cluster if required. The provided token should be presented in a UUID format.

This provides a mechanism to bootstrap ACLs without having any secrets present in Consul's
configuration files.
Expand Down Expand Up @@ -73,6 +73,23 @@ applications should ignore the `ID` field as it may be removed in a future major
}
```

### Sample Request with provided token


```json
{
"BootstrapSecret": "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
}
```

```shell-session
$ curl \
--request PUT \
--data @root-token.json \
http://127.0.0.1:8500/v1/acl/bootstrap
```


You can detect if something has interfered with the ACL bootstrapping process by
checking the response code. A 200 response means that the bootstrap was a success, and
a 403 means that the cluster has already been bootstrapped, at which point you should
Expand Down
Loading