v0.328.0: the catalog allow-list — the box refuses container rights a template asks for and the box does not give (R-971, D2 = A)
gates / gates (push) Successful in 45s
gates / gates (push) Successful in 45s
internal/privpolicy checks the RENDERED compose (privileged, cap_add, devices, host namespaces, security_opt,
sysctls, volumes_from, build/include/extends, env_file/config files outside the stack dir, aliasing named volumes,
binds outside the box's data roots, the Docker socket, propagation, ${VAR} in a privileged key) against an
allow-list compiled into the controller (one entry: homepage's read-only socket). Asked before every compose
write: the 15-minute sync, the guarded update/ladder step, the install, both settings changes, both restores
(before the stop). A refusal writes nothing, keeps the app on its last good compose, raises the operator-only
event app_definition_refused once per definition and puts one line on the app page. The sync never copies a
template named like a controller infra stack. Structural guard TestAllowList_IsWiredIntoEveryComposeWriter.
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package backup
|
||||
|
||||
// definitionAdmitter is the R-971 question a restore asks BEFORE it stops the app: would the unit's (or the snapshot's)
|
||||
// compose, rendered with the restore's env, pass the box's catalog allow-list? Implemented by main.go's stackAdapter;
|
||||
// a provider without it (the test fakes) asks nothing. A refusal leaves the live app running and untouched.
|
||||
type definitionAdmitter interface {
|
||||
AdmitDefinitionFromUnit(name, composeSrcDir string, fullEnv map[string]string) error
|
||||
}
|
||||
|
||||
// admitRestoredDefinition asks the provider when it can answer; nil when it cannot.
|
||||
func (m *Manager) admitRestoredDefinition(name, composeSrcDir string, env map[string]string) error {
|
||||
if a, ok := m.stackProvider.(definitionAdmitter); ok {
|
||||
return a.AdmitDefinitionFromUnit(name, composeSrcDir, env)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -905,6 +905,15 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
}
|
||||
}
|
||||
|
||||
// R-971: the snapshot's compose must pass the box's allow-list — asked before the stop, so a refusal leaves the
|
||||
// live app untouched.
|
||||
if defineFromSnapshot {
|
||||
if err := m.admitRestoredDefinition(stack, scratchCompose, snapEnv); err != nil {
|
||||
m.logger.Printf("[ERROR] [offbox] Restore REFUSED for %s: %v", stack, err)
|
||||
return res, util.MsgError("err.stacks.definition_refused", stack, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// --- FILES ----------------------------------------------------------------------------------
|
||||
// R-166: mark the stop→restore→start window BEFORE stopping. A controller killed anywhere inside
|
||||
// it used to leave the app down with nothing on disk recording that it was owed a restart — and a
|
||||
|
||||
@@ -415,6 +415,13 @@ func (m *Manager) RestoreFromRecoveryUnitAtWith(stackName, unitDir string, opt U
|
||||
return res, util.MsgError("err.backup.adatbazis_masolat_csonka_nem_indult", stackName)
|
||||
}
|
||||
|
||||
// R-971: the unit's compose must pass the box's allow-list — asked HERE, before the stop, so a refusal leaves the
|
||||
// live app running and untouched.
|
||||
if err := m.admitRestoredDefinition(stackName, composeDir, fullEnv); err != nil {
|
||||
m.logger.Printf("[ERROR] [backup] Restore REFUSED for %s: %v", stackName, err)
|
||||
return res, util.MsgError("err.stacks.definition_refused", stackName, err.Error())
|
||||
}
|
||||
|
||||
// Stop, restore named-volume data, recreate the definition, replay the DB with ONLY the database
|
||||
// service running, and only then start the whole stack.
|
||||
// F17: surface a data-restore failure instead of swallowing it (we still bring the app back up).
|
||||
|
||||
Reference in New Issue
Block a user