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:
parent
8242459c66
commit
cb5389cc89
|
@ -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.
|
||||
```
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,10 +207,25 @@ func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.AC
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret, err := lib.GenerateUUID(a.srv.checkTokenUUID)
|
||||
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{
|
||||
Token: structs.ACLToken{
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
15
api/acl.go
15
api/acl.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue