|
|
@@ -20,6 +20,20 @@ namespace Statum {
|
|
|
*/
|
|
|
public class StatumModule : Object, Module {
|
|
|
|
|
|
+ /**
|
|
|
+ * The "statum" config keys that carry long-lived secret material
|
|
|
+ * (SA-9): when any of them is present, the file's permissions
|
|
|
+ * become a finding.
|
|
|
+ */
|
|
|
+ private const string[] STATUM_SECRET_KEYS = {
|
|
|
+ "signing_secret_key",
|
|
|
+ "encryption_signing_secret_key",
|
|
|
+ "encryption_sealing_secret_key",
|
|
|
+ };
|
|
|
+
|
|
|
+ /** Paths already reported by {@link warn_on_readable_config}. */
|
|
|
+ private static GenericArray<string> reported_config_paths = new GenericArray<string>();
|
|
|
+
|
|
|
public void register_components(Container container) throws Error {
|
|
|
// Enable recursive mapping of ActionDto fields on typed slot models.
|
|
|
GObjectMapping.register_mapper<Model.ActionDto>(Model.ActionDto.get_mapper());
|
|
|
@@ -131,12 +145,79 @@ namespace Statum {
|
|
|
private static Astralis.WebConfigSection? read_statum_config(Inversion.Scope scope) {
|
|
|
try {
|
|
|
var config = scope.resolve<Astralis.WebConfig>();
|
|
|
- return config.get_section("statum");
|
|
|
+ var section = config.get_section("statum");
|
|
|
+ warn_on_readable_config(config, section);
|
|
|
+ return section;
|
|
|
} catch {
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * SA-9: warns when the files contributing the "statum" section
|
|
|
+ * (which carry every long-lived signing/sealing secret) are group-
|
|
|
+ * or world-readable.
|
|
|
+ *
|
|
|
+ * Deliberately a WARNING, not a refusal: refusing would brick
|
|
|
+ * development setups (where a 0644 config next to the sources is
|
|
|
+ * the norm and the only reader is the developer's own account) and
|
|
|
+ * break image-baked container deployments whose layers are already
|
|
|
+ * world-readable by construction. Production operators get a loud,
|
|
|
+ * actionable message instead.
|
|
|
+ */
|
|
|
+ private static void warn_on_readable_config(Astralis.WebConfig config, Astralis.WebConfigSection? section) {
|
|
|
+ if (section == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Only warn when the section actually carries a secret: an empty
|
|
|
+ // "statum" object (as `spry new` scaffolds it) has nothing to
|
|
|
+ // leak, and warning there would just train operators to ignore
|
|
|
+ // the message.
|
|
|
+ bool has_secret = false;
|
|
|
+ foreach (string key in STATUM_SECRET_KEYS) {
|
|
|
+ if (((!)section).get_string(key).length > 0) {
|
|
|
+ has_secret = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!has_secret) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var path in config.get_loaded_files()) {
|
|
|
+ if (reported_config_paths.find_with_equal_func(path, str_equal)) {
|
|
|
+ // Both provider factories read the same config; report
|
|
|
+ // each file once.
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ reported_config_paths.add(path);
|
|
|
+ try {
|
|
|
+ // Follow any symlink the operator made, so the mode of
|
|
|
+ // the file actually holding the secrets is reported.
|
|
|
+ var info = File.new_for_commandline_arg(path).query_info(
|
|
|
+ FileAttribute.STANDARD_TYPE + "," + FileAttribute.UNIX_MODE,
|
|
|
+ FileQueryInfoFlags.NONE);
|
|
|
+ // 0x1FF = 0777, 0x3F = 0067: group+other permission bits.
|
|
|
+ var mode = info.get_attribute_uint32(FileAttribute.UNIX_MODE) & 0x1FF;
|
|
|
+ if ((mode & 0x3F) != 0) {
|
|
|
+ var finding = ("%s is mode %04o — group/world readable.\n"
|
|
|
+ + "It carries the \"statum\" section: every session-signing\n"
|
|
|
+ + "and private-blob sealing key. Anyone who can read it\n"
|
|
|
+ + "can forge sessions.").printf(path, mode);
|
|
|
+ warning("[Statum] ────────────────────────────────────────────────────────────────\n"
|
|
|
+ + "[Statum] Insecure config file permissions (SA-9):\n"
|
|
|
+ + "[Statum] " + finding + "\n"
|
|
|
+ + "[Statum] Fix: chmod 600 " + path + "\n"
|
|
|
+ + "[Statum] ────────────────────────────────────────────────────────────────");
|
|
|
+ }
|
|
|
+ } catch (GLib.Error e) {
|
|
|
+ // A file that vanished or cannot be stat'ed is the
|
|
|
+ // loader's problem, not a permission finding.
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
/**
|