Skip to content

Commit be2cd1a

Browse files
authored
fix(config): treat allow_unverified_revocation_url as live in requires_reboot (#307)
The signer reads pki.allow_unverified_revocation_url from the stored config on every issuance, and nothing reads it at boot, so a change to it alone takes effect immediately. NeedsReboot still reported requires_reboot=true for it. Class it as hot next to pki.profiles and pki.root_leaf_issuance. Add tests that drive NeedsReboot through the real apply path: the running config read back from the store, and the incoming config after a protobuf wire round trip. They cover an identical re-apply and a profiles-only change (no reboot) and a network change (reboot). Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> Co-authored-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent 5f1c2c2 commit be2cd1a

4 files changed

Lines changed: 97 additions & 13 deletions

File tree

‎internal/config/reboot.go‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ import (
2828
// requires a reboot for the change to take effect.
2929
//
3030
// The CA signer reads the live config on every operation, so a change limited
31-
// to the cert profiles (pki.profiles) or the irreversible root-leaf-issuance
32-
// acknowledgement (pki.root_leaf_issuance) takes effect immediately for
33-
// signing — no reboot. Every other field (network, install disk, role, state
34-
// key, the revocation endpoint that gates the boot-time CRL/OCSP listener,
35-
// hostname, management link) is consumed at boot, so a change there still needs
36-
// a reboot. This is deliberately conservative: only the two proven-hot fields
37-
// are treated as hot; anything else falls through to "reboot".
31+
// to the cert profiles (pki.profiles), the irreversible root-leaf-issuance
32+
// acknowledgement (pki.root_leaf_issuance) or the revocation preflight override
33+
// (pki.allow_unverified_revocation_url) takes effect immediately for signing —
34+
// no reboot. Every other field (network, install disk, role, state key, the
35+
// revocation endpoint that gates the boot-time CRL/OCSP listener, hostname,
36+
// management link) is consumed at boot, so a change there still needs a
37+
// reboot. This is deliberately conservative: only the proven-hot fields are
38+
// treated as hot; anything else falls through to "reboot".
3839
func NeedsReboot(oldCfg, newCfg *Config) bool {
3940
if oldCfg == nil || newCfg == nil {
4041
return true
@@ -61,11 +62,12 @@ func Equivalent(a, b any) bool {
6162
}
6263

6364
// hotNormalized returns a copy of c with the hot-reconfigurable fields blanked,
64-
// so DeepEqual on two normalized configs is true exactly when the only
65-
// differences are hot fields.
65+
// so two normalized configs are Equivalent exactly when the only differences
66+
// are hot fields.
6667
func hotNormalized(c *Config) Config {
6768
n := *c
6869
n.PKI.Profiles = nil
6970
n.PKI.RootLeafIssuance = ""
71+
n.PKI.AllowUnverifiedRevocationURL = false
7072
return n
7173
}

‎internal/config/reboot_test.go‎

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ See the License for the specific language governing permissions and
1818
limitations under the License.
1919
*/
2020

21-
import "testing"
21+
import (
22+
"testing"
23+
24+
cryptosv1 "github.com/CryptOS-PKI/api/go/cryptos/v1"
25+
"google.golang.org/protobuf/proto"
26+
)
2227

2328
func baseConfig() *Config {
2429
return &Config{
@@ -112,3 +117,78 @@ func TestNeedsRebootIgnoresNilVersusEmpty(t *testing.T) {
112117
t.Error("a real protocol change was classified as live")
113118
}
114119
}
120+
121+
// roundTrip returns the two views of a config that ApplyConfig compares: the
122+
// running config as the node reads it back from its store, and the incoming
123+
// config, after mutate, as it arrives over the wire. The wire matters: an
124+
// empty list is written to the store as [] and reads back as an empty slice,
125+
// while the same list crossing gRPC decodes as nil.
126+
func roundTrip(t *testing.T, mutate func(*Config)) (*Config, *Config) {
127+
t.Helper()
128+
stored, err := Parse(validYAML(t))
129+
if err != nil {
130+
t.Fatalf("Parse: %v", err)
131+
}
132+
raw, err := stored.Marshal()
133+
if err != nil {
134+
t.Fatalf("Marshal: %v", err)
135+
}
136+
old, err := Parse(raw)
137+
if err != nil {
138+
t.Fatalf("Parse stored: %v", err)
139+
}
140+
incoming, err := Parse(raw)
141+
if err != nil {
142+
t.Fatalf("Parse incoming: %v", err)
143+
}
144+
mutate(incoming)
145+
wire, err := proto.Marshal(incoming.ToProto())
146+
if err != nil {
147+
t.Fatalf("proto.Marshal: %v", err)
148+
}
149+
var pb cryptosv1.MachineConfig
150+
if err := proto.Unmarshal(wire, &pb); err != nil {
151+
t.Fatalf("proto.Unmarshal: %v", err)
152+
}
153+
next, err := FromProto(&pb)
154+
if err != nil {
155+
t.Fatalf("FromProto: %v", err)
156+
}
157+
return old, next
158+
}
159+
160+
func TestNeedsRebootThroughTheStoreAndTheWire(t *testing.T) {
161+
t.Run("identical re-apply through the store and the API needs no reboot", func(t *testing.T) {
162+
old, next := roundTrip(t, func(*Config) {})
163+
if NeedsReboot(old, next) {
164+
t.Error("re-applying the running config must not require a reboot")
165+
}
166+
})
167+
168+
t.Run("profile-only change through the store and the API needs no reboot", func(t *testing.T) {
169+
old, next := roundTrip(t, func(c *Config) { c.PKI.Profiles = sampleProfiles() })
170+
if NeedsReboot(old, next) {
171+
t.Error("a profiles-only change must not require a reboot")
172+
}
173+
})
174+
175+
t.Run("network change through the store and the API requires a reboot", func(t *testing.T) {
176+
old, next := roundTrip(t, func(c *Config) { c.Network.Nameservers = []string{"192.0.2.53"} })
177+
if !NeedsReboot(old, next) {
178+
t.Error("adding a nameserver must require a reboot")
179+
}
180+
})
181+
}
182+
183+
func TestNeedsRebootLiveFields(t *testing.T) {
184+
t.Run("allow_unverified_revocation_url is read live by the signer (no reboot)", func(t *testing.T) {
185+
old, next := roundTrip(t, func(c *Config) {
186+
c.PKI.RevocationBaseURL = "http://192.0.2.10"
187+
c.PKI.AllowUnverifiedRevocationURL = true
188+
})
189+
old.PKI.RevocationBaseURL = "http://192.0.2.10"
190+
if NeedsReboot(old, next) {
191+
t.Error("an allow_unverified_revocation_url-only change must not require a reboot")
192+
}
193+
})
194+
}

‎internal/init/run.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ func boot(ctx context.Context, shutdown *shutdownRequests) (err error) {
472472
}
473473
// The signer reads the LIVE on-disk config, not the boot snapshot, so an
474474
// ApplyConfig change on a running node (a new/updated cert profile, the
475-
// root-leaf-issuance acknowledgement) takes effect for signing immediately
475+
// root-leaf-issuance acknowledgement, the revocation preflight override)
476+
// takes effect for signing immediately
476477
// without a reboot. Fall back to the boot config if the store is briefly
477478
// unreadable; install-level fields (network/disk/role/state key) are still
478479
// only consumed at boot, so reading them live here is harmless.

‎internal/node/providers.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,9 @@ func (c *ConfigStore) Apply(ctx context.Context, cfg *cryptosv1.MachineConfig) (
289289
// the node, and classifying whether the change needs a reboot.
290290
//
291291
// Classify BEFORE overwriting: a change limited to the hot-reconfigurable
292-
// fields (cert profiles, root-leaf-issuance acknowledgement) takes effect
293-
// live for signing, so the caller need not reboot. Any other change — or a
292+
// fields (cert profiles, root-leaf-issuance acknowledgement, revocation
293+
// preflight override) takes effect live for signing, so the caller need
294+
// not reboot. Any other change — or a
294295
// first apply with no prior config — requires a reboot. Fail safe to reboot
295296
// if the current config cannot be read or parsed.
296297
var oldCfg *config.Config

0 commit comments

Comments
 (0)