Allow Operator Generated bootstrap token (#14437)

Add support to provide an initial token via the bootstrap HTTP API, similar to hashicorp/nomad#12520
This commit is contained in:
Florian Apolloner 2023-01-04 21:19:33 +01:00 committed by GitHub
parent 8242459c66
commit cb5389cc89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 264 additions and 13 deletions

3
.changelog/14437.txt Normal file
View File

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

View File

@ -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"
)
@ -34,9 +35,19 @@ func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request)
return nil, aclDisabled
}
args := structs.DCSpecificRequest{
args := structs.ACLInitialTokenBootstrapRequest{
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 {

View File

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

View File

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

View File

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

View File

@ -1351,10 +1351,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 {

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package bootstrap
import (
"encoding/json"
"os"
"strings"
"testing"
@ -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")
}

View File

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

View File

@ -9,8 +9,8 @@ Command: `consul acl bootstrap`
Corresponding HTTP API Endpoint: [\[PUT\] /v1/acl/bootstrap](/api-docs/acl#bootstrap-acls)
The `acl bootstrap` command will request Consul to generate a new token with unlimited privileges to use
for management purposes and output its details. This can only be done once and afterwards bootstrapping
The `acl bootstrap` command generates a new token with unlimited privileges to use
for management purposes and outputs the token's details. Optionally, you can provide a Secret ID to use instead of generating a completely new token. You can create this bootstrapping token only once and afterwards bootstrapping
will be disabled. If all tokens are lost and you need to bootstrap again you can follow the bootstrap
[reset procedure](https://learn.hashicorp.com/consul/security-networking/acl-troubleshooting?utm_source=docs).
@ -24,7 +24,10 @@ are not supported from commands, but may be from the corresponding HTTP endpoint
## Usage
Usage: `consul acl bootstrap [options]`
Usage: `consul acl bootstrap [options] [FILE]`
If a file is supplied (or `-` for standard input), the new token's Secret ID is read from the file.
Otherwise, Consul creates a new one.
#### Command Options