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/acl"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,9 +35,19 @@ func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request)
|
||||||
return nil, aclDisabled
|
return nil, aclDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
args := structs.DCSpecificRequest{
|
args := structs.ACLInitialTokenBootstrapRequest{
|
||||||
Datacenter: s.agent.config.Datacenter,
|
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
|
var out structs.ACLToken
|
||||||
err := s.agent.RPC(req.Context(), "ACL.BootstrapTokens", &args, &out)
|
err := s.agent.RPC(req.Context(), "ACL.BootstrapTokens", &args, &out)
|
||||||
if err != nil {
|
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) {
|
func TestACL_HTTP(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("too slow for 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
|
// BootstrapTokens is used to perform a one-time ACL bootstrap operation on
|
||||||
// a cluster to get the first management token.
|
// 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 {
|
if err := a.aclPreCheck(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -207,10 +207,25 @@ func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.AC
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
secret, err := lib.GenerateUUID(a.srv.checkTokenUUID)
|
secret := args.BootstrapSecret
|
||||||
|
if secret == "" {
|
||||||
|
secret, err = lib.GenerateUUID(a.srv.checkTokenUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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{
|
req := structs.ACLTokenBootstrapRequest{
|
||||||
Token: structs.ACLToken{
|
Token: structs.ACLToken{
|
||||||
|
|
|
@ -38,7 +38,7 @@ func TestACLEndpoint_BootstrapTokens(t *testing.T) {
|
||||||
waitForLeaderEstablishment(t, srv)
|
waitForLeaderEstablishment(t, srv)
|
||||||
|
|
||||||
// Expect an error initially since ACL bootstrap is not initialized.
|
// Expect an error initially since ACL bootstrap is not initialized.
|
||||||
arg := structs.DCSpecificRequest{
|
arg := structs.ACLInitialTokenBootstrapRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
}
|
}
|
||||||
var out structs.ACLToken
|
var out structs.ACLToken
|
||||||
|
@ -72,6 +72,53 @@ func TestACLEndpoint_BootstrapTokens(t *testing.T) {
|
||||||
require.Equal(t, out.CreateIndex, out.ModifyIndex)
|
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) {
|
func TestACLEndpoint_ReplicationStatus(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
|
|
|
@ -1351,10 +1351,20 @@ type ACLTokenBatchDeleteRequest struct {
|
||||||
TokenIDs []string // Tokens to delete
|
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
|
// ACLTokenBootstrapRequest is used only at the Raft layer
|
||||||
// for ACL bootstrapping
|
// 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
|
// that bootstrapping must be performed but the actual token
|
||||||
// and the resetIndex will be generated by that RPC endpoint
|
// and the resetIndex will be generated by that RPC endpoint
|
||||||
type ACLTokenBootstrapRequest struct {
|
type ACLTokenBootstrapRequest struct {
|
||||||
|
|
15
api/acl.go
15
api/acl.go
|
@ -498,10 +498,25 @@ func (c *Client) ACL() *ACL {
|
||||||
return &ACL{c}
|
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
|
// Bootstrap is used to perform a one-time ACL bootstrap operation on a cluster
|
||||||
// to get the first management token.
|
// to get the first management token.
|
||||||
func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) {
|
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")
|
r := a.c.newRequest("PUT", "/v1/acl/bootstrap")
|
||||||
|
if btoken != "" {
|
||||||
|
r.obj = &BootstrapRequest{
|
||||||
|
BootstrapSecret: btoken,
|
||||||
|
}
|
||||||
|
}
|
||||||
rtt, resp, err := a.c.doRequest(r)
|
rtt, resp, err := a.c.doRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -3,10 +3,13 @@ package bootstrap
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/command/acl/token"
|
"github.com/hashicorp/consul/command/acl/token"
|
||||||
"github.com/hashicorp/consul/command/flags"
|
"github.com/hashicorp/consul/command/flags"
|
||||||
|
"github.com/hashicorp/consul/command/helpers"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,13 +46,34 @@ func (c *cmd) Run(args []string) int {
|
||||||
return 1
|
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()
|
client, err := c.http.APIClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
t, _, err := client.ACL().Bootstrap()
|
var t *api.ACLToken
|
||||||
|
t, _, err = client.ACL().BootstrapWithToken(boottoken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err))
|
c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err))
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -2,6 +2,7 @@ package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -87,3 +88,50 @@ func TestBootstrapCommand_JSON(t *testing.T) {
|
||||||
err := json.Unmarshal([]byte(output), &jsonOutput)
|
err := json.Unmarshal([]byte(output), &jsonOutput)
|
||||||
require.NoError(t, err, "token unmarshalling error")
|
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
|
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)
|
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
|
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,
|
cluster has not been bootstrapped previously. An operator created token can be provided in the body of the request to
|
||||||
and requires all Consul servers to be upgraded in order to operate.
|
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
|
This provides a mechanism to bootstrap ACLs without having any secrets present in Consul's
|
||||||
configuration files.
|
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
|
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
|
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
|
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)
|
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
|
The `acl bootstrap` command generates a new token with unlimited privileges to use
|
||||||
for management purposes and output its details. This can only be done once and afterwards bootstrapping
|
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
|
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).
|
[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
|
||||||
|
|
||||||
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
|
#### Command Options
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue