JeremiahSecrist / regula-nix

This project aims to help implement and enforce various security standards in NixOS. Regula meaning standards in latin.
https://jeremiahsecrist.github.io/regula-nix/
MIT License
12 stars 1 forks source link

Upload prototypical service. #5

Closed Skarlett closed 3 weeks ago

Skarlett commented 12 months ago

Include a minimal prototype to iron out architectural decisions.

Objectives:

Use systems in place.

Don't reinvent the wheel

{
  ....
  config = mkIf cfg.enable {
      assertions = [
        { assertion = cfg.database.passwordFile != null || cfg.database.password != "" || cfg.database.socket != null;
          message = "one of services.funkwhale.database.socket, services.funkwhale.database.passwordFile, or services.funkwhale.database.password must be set";
        }
        { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
          message = "services.funkwhale.database.user must be set to ${cfg.user} if services.funkwhale.database.createLocally is set true";
        }
        { assertion = cfg.database.createLocally -> cfg.database.socket != null;
          message = "services.funkwhale.database.socket must be set if services.funkwhale.database.createLocally is set to true";
        }
        { assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
          message = "services.funkwhale.database.host must be set to localhost if services.funkwhale.database.createLocally is set to true";
        }
      ];
}

Likewise extending pre-existing modules would be as the following

{
  config = mkIf cfg.enable {
     services.ssh.assertions = [
       { assertion = services.ssh.enable -> services.ssh.port != 22;
         message = "services.ssh.port != 22" }; 
     ];
  };
}

Discoverable assertions

Nix is not a reflective langauge so natively ensuring that assertions are discovered relies on hardcoding. As the examples above. Without a preparser, this ability is gated.

Metadata

Unspecified.

Prototypical example

# Core module
{ lib, ... }:
let cfg = config.cis;
in
{
  options.cis.mkEnableOption "use cis asserts";

  config = mkMerge [
     (lib.optional cfg.cis.ssh import ./ssh) 

  ];
}
# ssh.nix
{
  options.cis.ssh.enable = mkEnableOption "Enable cis ssh";

  config = mkIf cfg.enable {
     services.ssh.assertions = [
       { assertion = services.ssh.enable -> services.ssh.port != 22;
         message = "services.ssh.port != 22" }; 
     ];
  };
};
# level1-desktop.nix
{
  options.cis.ssh.enable = mkEnableOption "Enable cis ssh";

  config = mkIf cfg.enable {
     cis.ssh.enable = true;
  };
};
JeremiahSecrist commented 12 months ago

I like most of these ideas. however, certain aspects seem unanswered that I would cover and see mentioned in the above POC.

Handling warns vs assertions

One notion I was looking to solve is some security benchmarks are not strictly enforcing. But rather these rules are simply advised the user be made aware of the potential danger of x situation.

Extending Assertion Message

When the user enables a profile and builds the system one should be exposed to the following via the assert message:

sample assert / warn message: CIS server profile 1 version 2 page x: tmp must be a unique mount point.

Skarlett commented 12 months ago

One notion I was looking to solve is some security benchmarks are not strictly enforcing. But rather these rules are simply

advised the user be made aware of the potential danger of x situation.

This is expected to be internally handled by the module provided, by running the conditions needed to evaluate if the assertion would otherwise fail, and push its message by its self into config.<service>.warnings

When the user enables a profile and builds the system one should be exposed to the following via the assert message: what base rule failed (we get this for free)

This still needs to be composed, without relations to internal nixos attr paths.

what profile(s) enabled

In regards to what profile is responsible for the failed assertion? Showing all isn't necessary.

where to find it in upstream profiles documentation

Internal metadata per definition -- I believe to be handled internally by providing a submodule.

what organization that profile is assigned to

I believe this abstraction to be the last of concern and shouldn't be apart of the scope. Due to the nature of the module system, one would only need to define their own organizational standard by composing the components provided to them.

JeremiahSecrist commented 12 months ago

Don't reinvent the wheel It is not reinventing the wheel but extending it.

This is still using it as the back-end, however I want various meta data to be available during assertion fail time. meaning I don't want the user to have to dig through source code to find the error.

Again I want the assertion to point the user to: A) what assertion failed B) what profile enabled this assertion C) what organization ex. (CIS, STIGS, ...) D) where to find this rule upstream where by url or page number

These things are in scope as I want the audit process for a third part to be as easy as possible. if the assertion in play does not tell the user where the audit is only partially useful. Citations are necessary and should be covered in the structure. I don't expect upstream organizations and or companies to write their own assertions any time soon as such I will need to write them myself. I want the self written assertions and profiles to document at build time when and were these rules originated without needing the user to dive in the source code.

If you can expand you're current interface design to expect this kind of data and extend the original assertion I am more than willing to redesign it in this fashion.

Skarlett commented 12 months ago

I completely misunderstood what you were referring to as organizations and assumed that the subject was of commercial enterprises. The last item listed previously (in my post) should be considered withdrawn for further discussions.

User side

# Backend handlers.
regula.cis.enable = true;
regula.nist.enable = true;
regula.nccoe.enable = true;
# regula.enable-all = true;

regula.profile = "MAC-1_Public";  # "MAC-{1-3}_{Public|Sensitive|Classified}"

# Use custom stig database
regula.stigs = {
  url = "https://nist.gov/stig/download";
  sha256 = "...";
};

# Internally gets passed to `services.ssh.assertions` or `warnings`
# based on `regula.enforce-hell`
# Side note: The use of attr-maps here will not allow
# checks to be sequentially ordered [1 -> 2 -> 3] 
# If we'd like to maintain ordering, we'll have to use lists.
services.ssh.regula.custom-check = {
  # normally `CUSTOM` would be `CIS` or likewise, 
  # followed by lookup codes 
  message = "CUSTOM MAC-1 Public | RCON shouldn't be exposed publically to the internet ";
  assertions = config.services.minecraft.rcon -> !(builtins.elem config.services.minecraft.rcon-port cfg.firewall)

   # further abstractions such as
   # profile = "MAC-1_Public";
   # strict = true;
   # lookup = "1:5:2"
   # can be added here
};

regula.enforce-hell = true;

Distinction of root.

I've used services.ssh.regula as the root, but this can be pivoted off into its own namespace, but it requires remapping.

Theres a debate worth to be had if remapping the root onto something like regula.profiles.<something> could make it easier to query the system in nix repl

while the previous root has the advantage of being coherent at the end-user.

How do you feel about this perspective?

Is this an accurate depiction, or are am I missing more details that should be noted here?

Skarlett commented 12 months ago

Also side note. I would like to redefine some terms we've been using into more clear ones. Would one consider the following adjustments?

Profiles -> Mandatory access controls (MAC) Organization -> Compliance specifications (cspec)

I wouldn't be apposed to the continuance of regula.profile in regards to the source code.