PowerShell / DSC

This repo is for the DSC v3 project
MIT License
201 stars 29 forks source link

DSC meta configuration #282

Open michaeltlombardi opened 10 months ago

michaeltlombardi commented 10 months ago

Summary of the new feature / enhancement

As a user, I want to define default behaviors/options for dsc so that I don't need to pass the options to every invocation or define a function to do so for me.

Right now, there's no persistent way for a user to control how dsc behaves except for the DSC_RESOURCE_PATH environmental variable. Users may want to set the default output format for DSC to pretty-json, or only use manifests that have been signed, or disable specific providers (see #274).

As the options for how dsc should behave expand, these needs will compound - consider the WhatIf scenario, whether to pre-check for permissions, etc.

Proposed technical implementation details (optional)

I haven't done much research on the available options for rust applications, but in an ideal world, we could borrow (a subset of) the functionality that viper has, supporting layered overrides, where each item in the following list takes precedence over the item below it:

Even if we only supported flags and environment variables, I think we'd have a more manageable UX. We could implement the configuration file handling later, if ever. With support for using environment variables as default options, users could use .env files as lightweight configuration, or set the variables in their CI jobs, or whatever makes sense for their context.

anmenaga commented 10 months ago

My vote is for a json configuration file sitting next to dsc.exe, similar to what PowerShell is using - it's proven to be a good solution and easy to implement from dev perspective. We can even have a DSC resource to read/modify it, so that it won't require manual editing.

michaeltlombardi commented 10 months ago

I'm fine with either/or JSON or YAML, and we can readily schematize the configuration file for documenting/editing. I think a DSC Resource to read/modify it is perfectly sensible, could just be calling dsc meta get|set built into the CLI.

SteveL-MSFT commented 8 months ago

dsc should be a resource for its own config and as such the config file format can be either JSON or YAML

theJasonHelmick commented 3 weeks ago

DSC meta settings

This document describes potential settings for DSC's meta configuration. There are six scenarios,including examples, organized into near-term and future priorities.

[!NOTE] The settings in the examples use snake-case and YAML. This is for readability in the proposal, but actual naming should be more carefully considered. The example settings are illustrative, not prescriptive.

Configuration file

The configuration file that contains the meta settings should be located with dsc.exe for portable configurations, otherwise, should be located in the correct configuration location for that operating system. File can be json or yaml and configured by a DSC resource (planned future).

File name json: dsc.settings.json File name yaml: dsc.settings.yaml

P0 -----------------------

Resource discovery path

As a user, I want to pre-define the path DSC uses to look for resources and to control whether that path can be overridden with the DSC_RESOURCE_PATH environment variable, so that I can have more deterministic control over the resources invoked on my machine.

Users should be able to define an array of directories that DSC should search for non-built-in resources[^1]. The settings should:

Example settings

With an empty configuration file ```yaml # settings.dsc.yaml --- # Effective settings resource_path: allow_env_override: true append_env_path: true directories: null ```
With directories defined ```yaml # settings.dsc.yaml resource_path: directories: - '/ops/dsc/resources' - '/app/dsc' --- # Effective settings resource_path: allow_env_override: false append_env_path: false directories: - '/ops/dsc/resources' - '/app/dsc' ```
With directories defined and PATH appended. ```yaml # settings.dsc.yaml resource_path: append_env_path: true directories: - '/ops/dsc/resources' - '/app/dsc' --- # Effective settings resource_path: allow_env_override: false append_env_path: true directories: - '/ops/dsc/resources' - '/app/dsc' ```

Tracing

As a user, I want to define a default trace level and format for DSC instead of always passing the specified flags at runtime or using the DSC_TRACE_LEVEL environment variable.

Currently, the tracing defaults are built-in and can only be overridden by specifying the option flags on the root command or the DSC_TRACE_LEVEL environment variable (level only, not format). The settings should:

Example settings

When tracing isn't specified in settings ```yaml # settings.dsc.yaml --- # Effective settings tracing: level: warning format: default allow_override: true ```
When tracing.level is specified ```yaml # settings.dsc.yaml tracing: level: info --- # Effective settings tracing: level: info format: default allow_override: true ```
When tracing is fully specified ```yaml # settings.dsc.yaml tracing: level: info format: json allow_override: false --- # Effective settings tracing: level: info format: json allow_override: false ```

P1-P2 -----------------------

Allowed configuration documents and operations

As an infrastructure engineer, I want to limit DSC to only using approved configuration documents so that we can limit issues and how the documents change our system.

As an infrastructure engineer, I want to limit the dsc config <operation> commands that can be executed to ensure stronger control over how users can query and modify our systems.

The most coherent approach to this seems to be enabling users to define a list of allowed configuration documents with optional additional restrictions.

We could also define how users can send documents to DSC - the most reliable being allow-listed document files with a known SHA and trusted signature, but we can allow users to configure whether they allow arbitrary documents from stdin or as a JSON blob to an argument.

The settings should:

Example settings

When configuration isn't specified in settings ```yaml # settings.dsc.yaml --- # Effective settings configuration: operations: export: [from_input, from_document] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_input, from_document] set: [from_input, from_document] ```
With an allow list for configuration documents ```yaml # settings.dsc.yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [from_input, from_document] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_document] set: [from_document] ```
With an allow list and forbidding export ```yaml # settings.dsc.yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: operations: export: false # convenience, could also be empty array --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_document] set: [from_document] ```
With an allow list and boolean operation values ```yaml # settings.dsc.yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: operations: export: false get: true test: true whatIf: true set: true --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_document] set: [from_document] ```
With boolean operation values and without an allow list ```yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: operations: export: [] get: true test: true whatIf: true set: true --- # Effective settings configuration: operations: export: [] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_input, from_document] set: [from_input, from_document] ```
With fully explicit settings ```yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: operations: export: [] get: [from_document] test: [from_document] whatIf: [from_document] set: [from_document] --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [] get: [from_document] test: [from_document] whatIf: [from_document] set: [from_document] ```

Allow listing resources and invocation operations

As an infrastructure engineer, I want to define an allow list for resources that can be invoked or included in a configuration document.

As an infrastructure engineer, I want to limit the dsc resource <operation> commands that can be executed to ensure stronger control over how users can query and modify our systems.

The most coherent approach to this seems to be enabling users to define a list of allowed resources with optional additional restrictions. Users may prefer to define this list by path or by type or both. This check should be independent of discovery, which uses the resource_path settings.

For resource invocation, it probably makes the most sense to define a map of boolean values to operations, true enabling direct invocation and false forbidding it for a given operation. For simplicity, users should be able to specify true or false to allow/forbid all direct invocation operations.

These settings should:

Example settings

With an empty configuration file ```yaml # settings.dsc.yaml --- # Effective settings resource: allow_built_in: true allow_invocation: get: true test: true set: true export: true delete: true ```
With a list of allowed documents ```yaml # settings.dsc.yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [from_input, from_document] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_document] set: [from_document] resource: allow_built_in: true allow_invocation: get: true test: true set: false export: true delete: false ```
With a list of allowed documents and allow all invocations ```yaml # settings.dsc.yaml configuration: allow_documents: - /ops/dsc/configs/* # simple string as file glob - path: /app/dsc/web.dsc.json # object with advanced options sha256: resource: allow_invocation: true --- # Effective settings configuration: allow_documents: - path: /ops/dsc/configs/* - path: /app/dsc/web.dsc.json sha256: operations: export: [from_input, from_document] get: [from_input, from_document] test: [from_input, from_document] whatIf: [from_document] set: [from_document] resource: allow_built_in: true allow_invocation: get: true test: true set: false export: true delete: false ```
With all invocations forbidden ```yaml # settings.dsc.yaml resource: allow_invocation: false --- # Effective settings resource: allow_built_in: true allow_invocation: get: false test: false set: false export: false delete: false ```
With resources allowed by path ```yaml # settings.dsc.yaml resource: allow_paths: - /ops/dsc/resources/* # simple string as file glob - path: /app/dsc/resources/web.dsc.resource.json # object with advanced options sha256: --- # Effective settings resource: allow_built_in: true allow_paths: - path: /ops/dsc/resources/* - path: /app/dsc/resources/web.dsc.resource.json sha256: allow_invocation: get: false test: false set: false export: false delete: false ```
With resources allowed by type ```yaml # settings.dsc.yaml resource: allow_types: - Microsoft.SqlServer* # Simple string as type glob - type: TSToy* # object with advanced options require_signature: true --- # Effective settings resource: allow_built_in: true allow_types: - type: Microsoft.SqlServer* - type: TSToy* require_signature: true allow_invocation: get: false test: false set: false export: false delete: false ```
With resources allowed by path and type ```yaml # settings.dsc.yaml resource: allow_paths: - /ops/dsc/resources/* # simple string as file glob - path: /app/dsc/resources/web.dsc.resource.json # object with advanced options sha256: allow_types: - Microsoft.SqlServer* # Simple string as type glob - type: TSToy* # object with advanced options require_signature: true --- # Effective settings resource: allow_built_in: true allow_paths: - path: /ops/dsc/resources/* - path: /app/dsc/resources/web.dsc.resource.json sha256: allow_types: - type: Microsoft.SqlServer* - type: TSToy* require_signature: true allow_invocation: get: true test: true set: true export: true delete: true ```
resource:
  # Allow use of built-in resources, like `Microsoft.DSC/Group`
  allow_built_in: true # default
  # Define allowed resources by path glob
  allow_paths:
    # simple string as file glob
    - /ops/dsc/resources
    # object with advanced options
    - path:   /app/dsc/resources/web.dsc.resource.json
      sha256: <sha>
  # Define allowed resources by type glob
  allow_types:
      # Simle string as type glob
      - Microsoft.*
      # object with advanced options
      - type: Microsoft.SqlServer*
        require_signature: true
  allow_invocation:
    get:    true
    test:   true
    set:    true
    export: true
    delete: true

Require version pins in configurations

As an infrastructure engineer, I want to ensure that my configurations are as deterministic as possible without relying entirely on manual verification by having DSC reject configuration documents with resource instances that don't declare their version pin.

Currently, DSC doesn't support version pinning per resource instance or configuration. To help users ensure deterministic configurations, we should have an opt-in setting that requires version pins for resources in a configuration.

When DSC is set to require version pinning, it should fail the validation for any passed configuration that includes one or more instances without a version pin. The error should collect all such instances and report them in the error message, not just the first invalid resource instance.

Example settings

With an empty configuration file ```yaml # settings.dsc.yaml --- # Effective settings configuration: require_version_pinning: false ```
With the setting defined ```yaml # settings.dsc.yaml configuration: require_version_pinning: true --- # Effective settings configuration: require_version_pinning: true ```

Require verified documents and resources

As an infrastructure engineer, I want to require all configuration documents and resources to be signed by a trusted persona so that I can have increased trust in the resources that modify my systems.

As an infrastructure engineer, I want to require all configuration documents and resources to have a software bill of materials (SBOM) to comply with policies/regulations and have increased confidence in the provenance of code that modifies my systems.

This requires answering questions about signing configurations and resources, and having a trust model for DSC. This must be deferred until those domains are handled, but should be considered when implementing them.

If signatures are required, any operation that includes an unsigned configuration document or resource should raise an error indicating the document or resource isn't signed. Users should be able to specify whether to require signatures for either or both, and to override on a per-document level for explicitly allowed documents.

If SBOMs are required, any operation that includes a configuration or resource without one should raise an error indicating what items are missing an SBOM. Users should be able to specify whether to require SBOMs for either or both, and to override the setting on a per-resource level for explicitly allowed resources.

The following code snippet is a gesture in this direction, but I don't have the familiarity with signing or SBOMs to have a better idea at this time.

# Settings to define what DSC trusts for verifying signatures
verification:
  trusted_authorities:     # Define a list of authorities to use for verification
  trusted_personas:        # Define a subset of personas to trust
  require_signature: false # Define whether to require signatures for resources and documents
  require_sbom:      false # Define whether to require SBOMs for 
# Configuration settings
configuration:
  require_signature: true # Overrides verification.require_signature
  require_sbom:      true # Overrides verification.require_sbom
  allow_documents:
    # Allow all documents in this folder (require sbom and signature)
    - /ops/dsc/configs/*           
    # Allow all documents signed by this persona, regardless of path
    - signed_by: <trusted persona>  
    # Allow all documents in the folder & signed by the ops team
    - path: /ops/dsc/configs/*      
      signed_by: <ops team persona>
    # Allow this document without sbom, require signature
    - path: /temp/debug.dsc.config.yaml
      require_sbom: false
resource:
  require_signature: true # Overrides verification.require_signature
  require_sbom:      true # overrides verification.require_sbom
  allow_types:
    # Allow all resources in this namespace (require sbom and signature)
    - MyOrg*                       
    # Allow all resources signed by this persona, regardless of type
    - signed_by: <trusted persona> 
    # Allow all resources in this namespace signed by the ops team
    - type: MyOrg.Ops*             
      signed_by: <ops team persona>
    # Allow this resource without sbom, require signature
    - type: MyOrg.Debug/TelemetryTrace 

Remote discovery

[^1]: DSC currently defines the DSC_RESOURCE_PATH environment variable. We should consider renaming this to DSC_DISCOVERY_PATH if we plan to support publishing configurations or supporting extensions (#), or accept that we'll need to define new DSC_CONFIGURATION_PATH/DSC_EXTENSION_PATH environment variables later, or that DSC_RESOURCE_PATH will also be used to find non-resource items.

SteveL-MSFT commented 3 weeks ago

Per discussion for 3.0, we'll only implement the P0 items. Current thinking is to have a Microsoft.DSC/Config and Microsoft.DSC/Policy resources that handle scoping where Config is for the current user (the config file is stored in the same location as the cache) and Policy is for the instance of DSC (presumably can be installed system wide under a protected folder). Here Policy means the values cannot be overridden by the user while Config means it sets the default value, but can be overridden at runtime.

michaeltlombardi commented 3 weeks ago

I would recommend using Microsoft.DSC/Settings instead of Microsoft.DSC/Config to avoid further overloading the term config, if possible. The distinction between Policy and Settings is still intuitive to me.

jambar42 commented 3 weeks ago

I like the idea of having a Settings file with each resource / configuration used as an override to the higher level Settings. This is similar to how it is done in Bicep as well. This would allow for more complex configurations where some resources are allowed to change state, while other potentially disruptive SET operations are disallowed.