Skip to content

Commit a3c5730

Browse files
Fall back to local ca.json edit when --ca-url is unset
When `step ca provisioner add|update|remove` is given an existing `--ca-config` file but no `--ca-url`, edit the config locally instead of failing with a required `--ca-url`/`--root` error. Restores the documented offline bootstrap workflow broken since v0.23.1. Fixes #1705
1 parent b69dce7 commit a3c5730

2 files changed

Lines changed: 77 additions & 11 deletions

File tree

command/ca/provisioner/provisioner.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net"
7+
"os"
78
"time"
89

910
"github.com/pkg/errors"
@@ -105,6 +106,15 @@ type crudClient interface {
105106
func newCRUDClient(cliCtx *cli.Context, cfgFile string) (crudClient, error) {
106107
unauthAdminClient, err := cautils.NewUnauthenticatedAdminClient(cliCtx)
107108
if err != nil {
109+
// Documented offline workflow: `step ca provisioner add ... --ca-config
110+
// ca.json` edits the file locally and must not require --ca-url/--root
111+
// when those flags are absent. Fall back only when the config file
112+
// exists; otherwise keep the original admin-client error.
113+
if cfgFile != "" {
114+
if _, statErr := os.Stat(cfgFile); statErr == nil {
115+
return newLocalCaConfigClient(cfgFile)
116+
}
117+
}
108118
return nil, fmt.Errorf("error generating admin client: %w", err)
109119
}
110120

@@ -113,24 +123,28 @@ func newCRUDClient(cliCtx *cli.Context, cfgFile string) (crudClient, error) {
113123
err = unauthAdminClient.IsEnabled()
114124
switch {
115125
case errors.As(err, &netErr) || errors.Is(err, ca.ErrAdminAPINotImplemented):
116-
ui.PrintSelected("CA Configuration", cfgFile)
117-
cfg, err := config.LoadConfiguration(cfgFile)
118-
if err != nil {
119-
return nil, fmt.Errorf("error loading configuration: %w", err)
120-
}
121-
// Assume the ca.json is already valid to avoid enabling all the
122-
// features present in step-ca just to modify the provisioners.
123-
cfg.SkipValidation = true
124-
125-
ui.Println()
126-
return newCaConfigClient(context.Background(), cfg, cfgFile)
126+
return newLocalCaConfigClient(cfgFile)
127127
case errors.Is(err, ca.ErrAdminAPINotAuthorized):
128128
return cautils.NewAdminClient(cliCtx)
129129
default:
130130
return nil, err
131131
}
132132
}
133133

134+
func newLocalCaConfigClient(cfgFile string) (crudClient, error) {
135+
ui.PrintSelected("CA Configuration", cfgFile)
136+
cfg, err := config.LoadConfiguration(cfgFile)
137+
if err != nil {
138+
return nil, fmt.Errorf("error loading configuration: %w", err)
139+
}
140+
// Assume the ca.json is already valid to avoid enabling all the
141+
// features present in step-ca just to modify the provisioners.
142+
cfg.SkipValidation = true
143+
144+
ui.Println()
145+
return newCaConfigClient(context.Background(), cfg, cfgFile)
146+
}
147+
134148
func parseInstanceAge(ctx *cli.Context) (age string, err error) {
135149
if !ctx.IsSet("instance-age") {
136150
return

command/ca/provisioner/provisioner_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package provisioner
33
import (
44
"crypto/ed25519"
55
"crypto/rand"
6+
"flag"
7+
"fmt"
68
"net/netip"
79
"os"
10+
"path/filepath"
811
"testing"
912
"time"
1013

1114
nebula "github.com/slackhq/nebula/cert"
1215
"github.com/stretchr/testify/assert"
1316
"github.com/stretchr/testify/require"
17+
"github.com/urfave/cli"
1418
)
1519

1620
func TestReadNebulaRoots(t *testing.T) {
@@ -95,3 +99,51 @@ func serializeAndWriteNebulaCert(t *testing.T, tempDir string, cert nebula.Certi
9599

96100
return file.Name(), data
97101
}
102+
103+
func TestNewCRUDClient_CaConfigWithoutCaURL(t *testing.T) {
104+
t.Parallel()
105+
106+
dir := t.TempDir()
107+
cfgFile := filepath.Join(dir, "ca.json")
108+
// Paths need not exist: SkipValidation is set before authority.New.
109+
cfg := fmt.Sprintf(`{
110+
"root": %q,
111+
"crt": %q,
112+
"key": %q,
113+
"address": ":9000",
114+
"dnsNames": ["localhost"],
115+
"authority": {
116+
"provisioners": []
117+
}
118+
}
119+
`, filepath.Join(dir, "root.crt"), filepath.Join(dir, "intermediate.crt"), filepath.Join(dir, "intermediate.key"))
120+
require.NoError(t, os.WriteFile(cfgFile, []byte(cfg), 0o600))
121+
122+
app := cli.NewApp()
123+
set := flag.NewFlagSet("test", 0)
124+
_ = set.String("ca-url", "", "")
125+
_ = set.String("root", "", "")
126+
_ = set.String("ca-config", cfgFile, "")
127+
ctx := cli.NewContext(app, set, nil)
128+
129+
client, err := newCRUDClient(ctx, cfgFile)
130+
require.NoError(t, err)
131+
require.NotNil(t, client)
132+
_, ok := client.(*caConfigClient)
133+
require.True(t, ok, "expected local caConfigClient when --ca-config exists and --ca-url is unset")
134+
}
135+
136+
func TestNewCRUDClient_MissingCaURLWithoutConfig(t *testing.T) {
137+
t.Parallel()
138+
139+
app := cli.NewApp()
140+
set := flag.NewFlagSet("test", 0)
141+
_ = set.String("ca-url", "", "")
142+
_ = set.String("root", "", "")
143+
ctx := cli.NewContext(app, set, nil)
144+
145+
client, err := newCRUDClient(ctx, "")
146+
require.Error(t, err)
147+
require.Nil(t, client)
148+
require.Contains(t, err.Error(), "ca-url")
149+
}

0 commit comments

Comments
 (0)