Repository: getsops/sops (language: python)

Issue:
Add ability to strip encrypted or unencrypted suffix on decryption
This would allow the use of sops without needing to update key names in the areas that they are used to have `_encrypted` or `_unencrypted`.

Encrypted example:
```
db:
    password_encrypted: ENC[...]
```

Upon decryption with a "strip suffix option":
```
db:
    password: pw123
```
Move extraction of encryption and rotation options to separate functions
Preparation for a rewrite of https://github.com/getsops/sops/pull/1343, which aims at implementing https://github.com/getsops/sops/issues/1333 (Make encrypt, decrypt, rotate, set, and edit proper commands).

This simplifies the main command handler to a more manageable ~180 lines of code, and heavily reduces the copy'n'paste that would happen in an implementation like https://github.com/getsops/sops/pull/1343.

This is #1387 without #974. Once merged I'll update #1387 to contain a version of #974 rebased to the then current `main` branch.

Move extraction of encryption and rotation options to separate functions
Preparation for a rewrite of https://github.com/getsops/sops/pull/1343, which aims at implementing https://github.com/getsops/sops/issues/1333 (Make encrypt, decrypt, rotate, set, and edit proper commands).

This simplifies the main command handler to a more manageable ~180 lines of code, and heavily reduces the copy'n'paste that would happen in an implementation like https://github.com/getsops/sops/pull/1343.

This is #1387 without #974. Once merged I'll update #1387 to contain a version of #974 rebased to the then current `main` branch.

Support using comments to select parts to encrypt
This PR adds support to annotate comments with a string (e.g., `sops:enc`) which can then be matched with a regex. If it matches, the corresponding value (the one which follows the comment) is encrypted while other values are not. (There is also the opposite regex available, to select those values which should not be encrypted.)

This enables the YAML file to have the same structure encrypted and decrypted, without having to add suffixes or manage complex regexes to match keys. See #543 for more discussion.
Support using comments to select parts to encrypt
This PR adds support to annotate comments with a string (e.g., `sops:enc`) which can then be matched with a regex. If it matches, the corresponding value (the one which follows the comment) is encrypted while other values are not. (There is also the opposite regex available, to select those values which should not be encrypted.)

This enables the YAML file to have the same structure encrypted and decrypted, without having to add suffixes or manage complex regexes to match keys. See #543 for more discussion.
Move extraction of encryption and rotation options to separate functions
Preparation for a rewrite of https://github.com/getsops/sops/pull/1343, which aims at implementing https://github.com/getsops/sops/issues/1333 (Make encrypt, decrypt, rotate, set, and edit proper commands).

This simplifies the main command handler to a more manageable ~180 lines of code, and heavily reduces the copy'n'paste that would happen in an implementation like https://github.com/getsops/sops/pull/1343.

This is #1387 without #974. Once merged I'll update #1387 to contain a version of #974 rebased to the then current `main` branch.

Support using comments to select parts to encrypt
This PR adds support to annotate comments with a string (e.g., `sops:enc`) which can then be matched with a regex. If it matches, the corresponding value (the one which follows the comment) is encrypted while other values are not. (There is also the opposite regex available, to select those values which should not be encrypted.)

This enables the YAML file to have the same structure encrypted and decrypted, without having to add suffixes or manage complex regexes to match keys. See #543 for more discussion.


Discussion / hints:
Following https://github.com/mozilla/sops/issues/539 I'm going to work on this!  
I'm with @jvehent here https://github.com/mozilla/sops/issues/539#issuecomment-533555867. I'm not sure this is SOPS's job.

It's not that I don't think your use case is valid. As maintainers, I think we have to be careful what features we allow into our code base, since once they're in there, we basically have to support them forever.


I do understand that, but requiring everyone using that feature to have a wrapper script to remove those suffix if very painful for the users. As `sops` is already performing such a removal for key metadata, I would expect it to remove also the suffix (I would agree with @davidovich https://github.com/mozilla/sops/issues/539#issuecomment-533559825 in considering that a sops level metadata). 

> As maintainers, I think we have to be careful what features we allow into our code base,

As I'm not a maintainer, is not in my interest to just push some code without approval, but if your fear the maintenance costs may be worth discussing this looking at a possible implementation? 🙂  
I will gladly look at it, but I can make no guarantee that it will be merged. 
I understand the temptation to bake every use case into a single tool, but I really think you should follow the unix approach here, and make a simple separate tool that edits your yaml files the way you want.

Sops is a security tool, and every non-security use-case we support into it makes it larger and more difficult to audit. 
I would also want to have this feature.

It messes my sops integration with existing tooling and writing another layer of transformation introduces unnecessary complexity. 

Specifically, in my use-case where I use Ansible @endorama's `sops_vars` plugin [from here](https://github.com/ansible/ansible/pull/59641), I would have to add additional transformation logic on top of that either via new plugin or bake-it in `sops_vars` plugin itself (but that is not the job for this plugin). Another way is to change the variable name in all existing playbook tasks.

I would agree with @endorama 
> As `sops` is already performing such a removal for key metadata, I would expect it to remove also the suffix

(I intuitively expected the removal of the suffix but discovered that this isn't a case)

and agree with @davidovich [#539 (comment)](https://github.com/mozilla/sops/issues/539#issuecomment-533559825) about threating it as `metadata`/`tag`.

Currently, I'm using "workaround" to list all items that I want to encrypt them in `encrypted_regex: (password|api_secret|access_token)`, but with `encrypted_suffix` I would have better control which properties to encrypt or not.
> Sops is a security tool, and every non-security use-case we support into it makes it larger and more difficult to audit.

I would agree if this feature was not already present, in this case I believe is "partially" present: this missing piece makes using this feature a burden in different use cases.  
I do agree that a reduced surface is good for security but IMO security tools should also be easy, intuitive and functional to use.

I'd like to highlight a couple of use cases that are currently only achievable with `encrypted-regex` or cannot be achieved at all.  
(I would prefer *not* to require my team to use regex, they are brittle and fragile)

The first use case is Kubernetes secrets. As the Kubernetes API server has no clue what `_encrypted` or `_unencrypted` means, you either encrypt the entire file or use `encrypted-regex`.  
The second use case is Helm chart secrets (for example in user supplied `values.yaml` files). In this case encrypting the entire file makes no sense, and regex can be impossible to write properly (YAML is structured and is possible to have secrets in different places across the file). 

In both cases requiring an external tool makes everything more fragile and complex.  
It's a pity that this suffix feature, **very** useful because ensure integrity even for unencrypted values, cannot be used in such use cases.

We can live without it, and I will happily contribute to other piece of the codebase, but please consider allowing this. Will make sops easier to integrate and more effective for your users.
> The second use case is Helm chart secrets (for example in user supplied `values.yaml` files). In this case encrypting the entire file makes no sense, and regex can be impossible to write properly (YAML is structured and is possible to have secrets in different places across the file).

FWIW, I've seen people keep two `.yaml` files, one for secrets, and one for non-secrets. You can pass helm additional values files, so that would solve this problem.

> FWIW, I've seen people keep two `.yaml` files, one for secrets, and one for non-secrets. You can pass helm additional values files, so that would solve this problem.

I thought about that, but it is not always achievable. For example:

If you use `var` files in ansible in format
> *params.yml*
> ```
> database_hostname: example.com
> database_login_username: admin
> ```

> *secrets.yml*
> ```
> database_login_password: admin_password
> ```
 These files would be merged (variables with the same name in both files overwritten, but this is not a case) and the end result would be:
```
database_hostname: example.com
database_login_username: admin
database_login_password: admin_password
```
sops encrypt `secrets.yml` file and :tada: 

**BUT**

for people who use `hash`/`dictionary` format
> *params.yml*
> ```
> database:
>   hostname: example.com
>   login:
>     username: admin
>     password: admin_password
> ```

and when they split it into different files
> *params.yml*
> ```
> database:
>   hostname: example.com
>   login:
>     username: admin
> ```

> *secrets.yml*
> ```
> database:
>   login:
>     password: admin_password
> ```

End result would be
```
database:
  login:
    password: admin_password
```
as overwrites previously defined dictionaries.

Arguably you could change Ansible `ANSIBLE_HASH_BEHAVIOUR` to `merge` (described [here](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-hash-behaviour)) and have desired end result 

but as their documentation says:

> This setting controls how variables merge in Ansible. By default Ansible will override variables in specific precedence orders, as described in Variables. When a variable of higher precedence wins, it will replace the other value. Some users prefer that variables that are hashes (aka ‘dictionaries’ in Python terms) are merged. This setting is called ‘merge’. **This is not the default behavior** and it does not affect variables whose values are scalars (integers, strings) or arrays. **We generally recommend not using this setting unless you think you have an absolute need for it**, and playbooks in the official examples repos do not use this setting In version 2.0 a combine filter was added to allow doing this for a particular variable (described in Filters).

because it can cause unwanted issues with playbook that are not written in mind with this behavior.

What are security/maintainability/other concerns that prevent adding this feature?
> It messes my sops integration with existing tooling and writing another layer of transformation introduces unnecessary complexity.

That complexity is located in a separate script though, and does not make the maintainability of Sops more difficult.

The point I'm trying to make is that, while I'm sympathetic with your needs and use cases, we can't have a command line flag for every use case that people have. It's impossible to maintain down the road, and we'll end up breaking user workflows when we inevitably deprecate some of these functionalities.

Sops must focus on security: adding new encryption formats, or integrating with new KMS, and guaranteeing a high level of security for encrypted files. I think it's reasonable to say that file transformation use cases are out of scope and should be handled by external scripts, simply because we cannot predict how every user will want these transformations to happen.
As a compromise, maybe we could have a script that ships within the sops binary that can be used for this, but is split off from all other functionality in a subcommand. What do you think @jvehent ?
I'm open to ideas, and I'm not completely closed to merging the stuff in main sops, depending on complexity of the patch and where we think we should draw the line.
I agree that sops can't support every use case. But the fact is that the `--encrypted-suffix` and `--unencrypted-suffix` exist, and that no tool can understand a file with these suffixes. So IMHO I think if someone uses these prefixes, he wants that these suffixes are no longer present in decrypted files. I would even say that I don't see a case where we might want to keep them.
An option to remove these prefixes would be good and backward compatible.
It seems like there is a decent amount of people that want something like this. I think most people who have contributed to the SOPS codebase would agree that it is more complex than it needs to because we have added too many features too quickly, even when only one person found it useful.

I think I would be okay with what I mention in https://github.com/mozilla/sops/issues/543#issuecomment-541230066, but I would prefer to avoid a flag, since that is where most of the clutter in our CLI is. Something like `sops -o foo.yaml -d foo.enc.yaml; sops stripsuffixes foo.yaml` would be acceptable to me, since it can be kept fairly separate in code.
Not speaking against this feature, but if you want this functionality you might use [jq](https://github.com/stedolan/jq/issues) for key-remapping in the meantime
@autrilla - would it be possible instead to make it so you can just annotate certain secrets with a comment that SOPS knows to look for ahead of time (maybe provided in `.sops.yaml`?) that can indicate when to keep things unencrypted?
Yes, I think comment-based annotation of fields to encrypt (or not) would be probably the best approach here. The beauty of it is that comments can stay in decrypted version as well, so when you re-encrypt the file (after editing, for example), sops can know which fields it has to encrypt and which not. If you remove suffix you remove this information.







Fix patch:
diff --git a/README.rst b/README.rst
index 042999af3..0c8e4bab4 100644
--- a/README.rst
+++ b/README.rst
@@ -1565,9 +1565,19 @@ that match the supplied regular expression. For example, this command:
 will not encrypt the values under the ``description`` and ``metadata`` keys in a YAML file
 containing kubernetes secrets, while encrypting everything else.
 
+For YAML files, another method is to use ``--encrypted-comment-regex`` which will
+only encrypt comments and values which have a preceding comment matching the supplied
+regular expression.
+
+Conversely, you can opt in to only left certain keys without encrypting by using the
+``--unencrypted-comment-regex`` option, which will leave the values and comments
+unencrypted when they have a preeceding comment, or a trailing comment on the same line,
+that matches the supplied regular expression.
+
 You can also specify these options in the ``.sops.yaml`` config file.
 
-Note: these four options ``--unencrypted-suffix``, ``--encrypted-suffix``, ``--encrypted-regex`` and ``--unencrypted-regex`` are
+Note: these six options ``--unencrypted-suffix``, ``--encrypted-suffix``, ``--encrypted-regex``,
+``--unencrypted-regex``, ``--encrypted-comment-regex``, and ``--unencrypted-comment-regex`` are
 mutually exclusive and cannot all be used in the same file.
 
 Encryption Protocol
diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go
index cd6008f17..ace7d8c2c 100644
--- a/cmd/sops/encrypt.go
+++ b/cmd/sops/encrypt.go
@@ -15,13 +15,15 @@ import (
 )
 
 type encryptConfig struct {
-	UnencryptedSuffix string
-	EncryptedSuffix   string
-	UnencryptedRegex  string
-	EncryptedRegex    string
-	MACOnlyEncrypted  bool
-	KeyGroups         []sops.KeyGroup
-	GroupThreshold    int
+	UnencryptedSuffix       string
+	EncryptedSuffix         string
+	UnencryptedRegex        string
+	EncryptedRegex          string
+	UnencryptedCommentRegex string
+	EncryptedCommentRegex   string
+	MACOnlyEncrypted        bool
+	KeyGroups               []sops.KeyGroup
+	GroupThreshold          int
 }
 
 type encryptOpts struct {
@@ -61,14 +63,16 @@ func ensureNoMetadata(opts encryptOpts, branch sops.TreeBranch) error {
 
 func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata {
 	return sops.Metadata{
-		KeyGroups:         config.KeyGroups,
-		UnencryptedSuffix: config.UnencryptedSuffix,
-		EncryptedSuffix:   config.EncryptedSuffix,
-		UnencryptedRegex:  config.UnencryptedRegex,
-		EncryptedRegex:    config.EncryptedRegex,
-		MACOnlyEncrypted:  config.MACOnlyEncrypted,
-		Version:           version.Version,
-		ShamirThreshold:   config.GroupThreshold,
+		KeyGroups:               config.KeyGroups,
+		UnencryptedSuffix:       config.UnencryptedSuffix,
+		EncryptedSuffix:         config.EncryptedSuffix,
+		UnencryptedRegex:        config.UnencryptedRegex,
+		EncryptedRegex:          config.EncryptedRegex,
+		UnencryptedCommentRegex: config.UnencryptedCommentRegex,
+		EncryptedCommentRegex:   config.EncryptedCommentRegex,
+		MACOnlyEncrypted:        config.MACOnlyEncrypted,
+		Version:                 version.Version,
+		ShamirThreshold:         config.GroupThreshold,
 	}
 }
 
diff --git a/cmd/sops/main.go b/cmd/sops/main.go
index bc888428d..155eaec78 100644
--- a/cmd/sops/main.go
+++ b/cmd/sops/main.go
@@ -1581,6 +1581,14 @@ func main() {
 			Name:  "encrypted-regex",
 			Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
 		},
+		cli.StringFlag{
+			Name:  "unencrypted-comment-regex",
+			Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.",
+		},
+		cli.StringFlag{
+			Name:  "encrypted-comment-regex",
+			Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.",
+		},
 		cli.StringFlag{
 			Name:  "config",
 			Usage: "path to sops' config file. If set, sops will not search for the config file recursively.",
@@ -1839,6 +1847,8 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
 	encryptedSuffix := c.String("encrypted-suffix")
 	encryptedRegex := c.String("encrypted-regex")
 	unencryptedRegex := c.String("unencrypted-regex")
+	encryptedCommentRegex := c.String("encrypted-comment-regex")
+	unencryptedCommentRegex := c.String("unencrypted-comment-regex")
 	macOnlyEncrypted := c.Bool("mac-only-encrypted")
 	conf, err := loadConfig(c, fileName, nil)
 	if err != nil {
@@ -1858,6 +1868,12 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
 		if unencryptedRegex == "" {
 			unencryptedRegex = conf.UnencryptedRegex
 		}
+		if encryptedCommentRegex == "" {
+			encryptedCommentRegex = conf.EncryptedCommentRegex
+		}
+		if unencryptedCommentRegex == "" {
+			unencryptedCommentRegex = conf.UnencryptedCommentRegex
+		}
 		if !macOnlyEncrypted {
 			macOnlyEncrypted = conf.MACOnlyEncrypted
 		}
@@ -1876,9 +1892,15 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
 	if unencryptedRegex != "" {
 		cryptRuleCount++
 	}
+	if encryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
+	if unencryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
 
 	if cryptRuleCount > 1 {
-		return encryptConfig{}, common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, or unencrypted_regex in the same file", codes.ErrorConflictingParameters)
+		return encryptConfig{}, common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters)
 	}
 
 	// only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided
@@ -1899,13 +1921,15 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
 	}
 
 	return encryptConfig{
-		UnencryptedSuffix: unencryptedSuffix,
-		EncryptedSuffix:   encryptedSuffix,
-		UnencryptedRegex:  unencryptedRegex,
-		EncryptedRegex:    encryptedRegex,
-		MACOnlyEncrypted:  macOnlyEncrypted,
-		KeyGroups:         groups,
-		GroupThreshold:    threshold,
+		UnencryptedSuffix:       unencryptedSuffix,
+		EncryptedSuffix:         encryptedSuffix,
+		UnencryptedRegex:        unencryptedRegex,
+		EncryptedRegex:          encryptedRegex,
+		UnencryptedCommentRegex: unencryptedCommentRegex,
+		EncryptedCommentRegex:   encryptedCommentRegex,
+		MACOnlyEncrypted:        macOnlyEncrypted,
+		KeyGroups:               groups,
+		GroupThreshold:          threshold,
 	}, nil
 }
 
diff --git a/config/config.go b/config/config.go
index 643268682..7565091fd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -134,21 +134,23 @@ type destinationRule struct {
 }
 
 type creationRule struct {
-	PathRegex         string `yaml:"path_regex"`
-	KMS               string
-	AwsProfile        string `yaml:"aws_profile"`
-	Age               string `yaml:"age"`
-	PGP               string
-	GCPKMS            string     `yaml:"gcp_kms"`
-	AzureKeyVault     string     `yaml:"azure_keyvault"`
-	VaultURI          string     `yaml:"hc_vault_transit_uri"`
-	KeyGroups         []keyGroup `yaml:"key_groups"`
-	ShamirThreshold   int        `yaml:"shamir_threshold"`
-	UnencryptedSuffix string     `yaml:"unencrypted_suffix"`
-	EncryptedSuffix   string     `yaml:"encrypted_suffix"`
-	UnencryptedRegex  string     `yaml:"unencrypted_regex"`
-	EncryptedRegex    string     `yaml:"encrypted_regex"`
-	MACOnlyEncrypted  bool       `yaml:"mac_only_encrypted"`
+	PathRegex               string `yaml:"path_regex"`
+	KMS                     string
+	AwsProfile              string `yaml:"aws_profile"`
+	Age                     string `yaml:"age"`
+	PGP                     string
+	GCPKMS                  string     `yaml:"gcp_kms"`
+	AzureKeyVault           string     `yaml:"azure_keyvault"`
+	VaultURI                string     `yaml:"hc_vault_transit_uri"`
+	KeyGroups               []keyGroup `yaml:"key_groups"`
+	ShamirThreshold         int        `yaml:"shamir_threshold"`
+	UnencryptedSuffix       string     `yaml:"unencrypted_suffix"`
+	EncryptedSuffix         string     `yaml:"encrypted_suffix"`
+	UnencryptedRegex        string     `yaml:"unencrypted_regex"`
+	EncryptedRegex          string     `yaml:"encrypted_regex"`
+	UnencryptedCommentRegex string     `yaml:"unencrypted_comment_regex"`
+	EncryptedCommentRegex   string     `yaml:"encrypted_comment_regex"`
+	MACOnlyEncrypted        bool       `yaml:"mac_only_encrypted"`
 }
 
 func NewStoresConfig() *StoresConfig {
@@ -169,15 +171,17 @@ func (f *configFile) load(bytes []byte) error {
 
 // Config is the configuration for a given SOPS file
 type Config struct {
-	KeyGroups         []sops.KeyGroup
-	ShamirThreshold   int
-	UnencryptedSuffix string
-	EncryptedSuffix   string
-	UnencryptedRegex  string
-	EncryptedRegex    string
-	MACOnlyEncrypted  bool
-	Destination       publish.Destination
-	OmitExtensions    bool
+	KeyGroups               []sops.KeyGroup
+	ShamirThreshold         int
+	UnencryptedSuffix       string
+	EncryptedSuffix         string
+	UnencryptedRegex        string
+	EncryptedRegex          string
+	UnencryptedCommentRegex string
+	EncryptedCommentRegex   string
+	MACOnlyEncrypted        bool
+	Destination             publish.Destination
+	OmitExtensions          bool
 }
 
 func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) {
@@ -283,9 +287,15 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
 	if rule.EncryptedRegex != "" {
 		cryptRuleCount++
 	}
+	if rule.UnencryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
+	if rule.EncryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
 
 	if cryptRuleCount > 1 {
-		return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, or unencrypted_regex for the same rule")
+		return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex for the same rule")
 	}
 
 	groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext)
@@ -294,13 +304,15 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
 	}
 
 	return &Config{
-		KeyGroups:         groups,
-		ShamirThreshold:   rule.ShamirThreshold,
-		UnencryptedSuffix: rule.UnencryptedSuffix,
-		EncryptedSuffix:   rule.EncryptedSuffix,
-		UnencryptedRegex:  rule.UnencryptedRegex,
-		EncryptedRegex:    rule.EncryptedRegex,
-		MACOnlyEncrypted:  rule.MACOnlyEncrypted,
+		KeyGroups:               groups,
+		ShamirThreshold:         rule.ShamirThreshold,
+		UnencryptedSuffix:       rule.UnencryptedSuffix,
+		EncryptedSuffix:         rule.EncryptedSuffix,
+		UnencryptedRegex:        rule.UnencryptedRegex,
+		EncryptedRegex:          rule.EncryptedRegex,
+		UnencryptedCommentRegex: rule.UnencryptedCommentRegex,
+		EncryptedCommentRegex:   rule.EncryptedCommentRegex,
+		MACOnlyEncrypted:        rule.MACOnlyEncrypted,
 	}, nil
 }
 
diff --git a/sops.go b/sops.go
index 2063607a0..4b97292a1 100644
--- a/sops.go
+++ b/sops.go
@@ -285,24 +285,24 @@ func (branch TreeBranch) Truncate(path []interface{}) (interface{}, error) {
 	return current, nil
 }
 
-func (branch TreeBranch) walkValue(in interface{}, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) (interface{}, error) {
+func (branch TreeBranch) walkValue(in interface{}, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) (interface{}, error) {
 	switch in := in.(type) {
 	case string:
-		return onLeaves(in, path)
+		return onLeaves(in, path, commentsStack)
 	case []byte:
-		return onLeaves(string(in), path)
+		return onLeaves(string(in), path, commentsStack)
 	case int:
-		return onLeaves(in, path)
+		return onLeaves(in, path, commentsStack)
 	case bool:
-		return onLeaves(in, path)
+		return onLeaves(in, path, commentsStack)
 	case float64:
-		return onLeaves(in, path)
+		return onLeaves(in, path, commentsStack)
 	case Comment:
-		return onLeaves(in, path)
+		return onLeaves(in, path, commentsStack)
 	case TreeBranch:
-		return branch.walkBranch(in, path, onLeaves)
+		return branch.walkBranch(in, path, commentsStack, onLeaves)
 	case []interface{}:
-		return branch.walkSlice(in, path, onLeaves)
+		return branch.walkSlice(in, path, commentsStack, onLeaves)
 	case nil:
 		// the value returned remains the same since it doesn't make
 		// sense to encrypt or decrypt a nil value
@@ -312,21 +312,38 @@ func (branch TreeBranch) walkValue(in interface{}, path []string, onLeaves func(
 	}
 }
 
-func (branch TreeBranch) walkSlice(in []interface{}, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) ([]interface{}, error) {
+func (branch TreeBranch) walkSlice(in []interface{}, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) ([]interface{}, error) {
+	// Because append returns a new slice, the original stack is not changed.
+	commentsStack = append(commentsStack, []string{})
 	for i, v := range in {
-		newV, err := branch.walkValue(v, path, onLeaves)
+		c, vIsComment := v.(Comment)
+		if vIsComment {
+			// If v is a comment, we add it to the slice of active comments.
+			// This allows us to also encrypt comments themselves by enabling encryption in a prior comment.
+			commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value)
+		}
+		newV, err := branch.walkValue(v, path, commentsStack, onLeaves)
 		if err != nil {
 			return nil, err
 		}
 		in[i] = newV
+		if !vIsComment {
+			// If v is not a comment, we clear the slice of active comments.
+			commentsStack[len(commentsStack)-1] = []string{}
+		}
 	}
 	return in, nil
 }
 
-func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) (TreeBranch, error) {
+func (branch TreeBranch) walkBranch(in TreeBranch, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) (TreeBranch, error) {
+	// Because append returns a new slice, the original stack is not changed.
+	commentsStack = append(commentsStack, []string{})
 	for i, item := range in {
-		if _, ok := item.Key.(Comment); ok {
-			enc, err := branch.walkValue(item.Key, path, onLeaves)
+		if c, ok := item.Key.(Comment); ok {
+			// If key is a comment, we add it to the slice of active comments.
+			// This allows us to also encrypt comments themselves by enabling encryption in a prior comment.
+			commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value)
+			enc, err := branch.walkValue(item.Key, path, commentsStack, onLeaves)
 			if err != nil {
 				return nil, err
 			}
@@ -340,26 +357,113 @@ func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(
 				return nil, fmt.Errorf("walkValue of Comment should be either Comment or string, was %T", enc)
 			}
 		}
+		c, valueIsComment := item.Value.(Comment)
+		if valueIsComment {
+			// If value is a comment, we add it to the slice of active comments.
+			// This allows us to also encrypt comments themselves by enabling encryption in a prior comment.
+			commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value)
+		}
 		key, ok := item.Key.(string)
 		if !ok {
 			return nil, fmt.Errorf("Tree contains a non-string key (type %T): %s. Only string keys are"+
 				"supported", item.Key, item.Key)
 		}
-		newV, err := branch.walkValue(item.Value, append(path, key), onLeaves)
+		newV, err := branch.walkValue(item.Value, append(path, key), commentsStack, onLeaves)
 		if err != nil {
 			return nil, err
 		}
 		in[i].Value = newV
+		if !valueIsComment {
+			// If value is not a comment, we clear the slice of active comments.
+			commentsStack[len(commentsStack)-1] = []string{}
+		}
 	}
 	return in, nil
 }
 
+func (tree Tree) shouldBeEncrypted(path []string, commentsStack [][]string, isComment bool) bool {
+	encrypted := true
+	if tree.Metadata.UnencryptedSuffix != "" {
+		for _, v := range path {
+			if strings.HasSuffix(v, tree.Metadata.UnencryptedSuffix) {
+				encrypted = false
+				break
+			}
+		}
+	}
+	if tree.Metadata.EncryptedSuffix != "" {
+		encrypted = false
+		for _, v := range path {
+			if strings.HasSuffix(v, tree.Metadata.EncryptedSuffix) {
+				encrypted = true
+				break
+			}
+		}
+	}
+	if tree.Metadata.UnencryptedRegex != "" {
+		for _, p := range path {
+			matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p))
+			if matched {
+				encrypted = false
+				break
+			}
+		}
+	}
+	if tree.Metadata.EncryptedRegex != "" {
+		encrypted = false
+		for _, p := range path {
+			matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p))
+			if matched {
+				encrypted = true
+				break
+			}
+		}
+	}
+	if tree.Metadata.UnencryptedCommentRegex != "" {
+	unencryptedComments:
+		for _, cs := range commentsStack {
+			for _, c := range cs {
+				matched, _ := regexp.Match(tree.Metadata.UnencryptedCommentRegex, []byte(c))
+				if matched {
+					encrypted = false
+					break unencryptedComments
+				}
+			}
+		}
+	}
+	if tree.Metadata.EncryptedCommentRegex != "" {
+		lenCommentsStack := len(commentsStack)
+		lenLastCommentsStack := len(commentsStack[lenCommentsStack-1])
+		encrypted = false
+	encryptedComments:
+		for i, cs := range commentsStack {
+			for j, c := range cs {
+				// A special case. We do not encrypt the comment line itself which matches the regex.
+				// So we skip the last line of the last set of comments. Only if the matches any previous
+				// line, we encrypt this comment. Otherwise we do not.
+				if isComment && i == lenCommentsStack-1 && j == lenLastCommentsStack-1 {
+					continue
+				}
+				matched, _ := regexp.Match(tree.Metadata.EncryptedCommentRegex, []byte(c))
+				if matched {
+					encrypted = true
+					break encryptedComments
+				}
+			}
+		}
+	}
+	return encrypted
+}
+
 // Encrypt walks over the tree and encrypts all values with the provided cipher,
 // except those whose key ends with the UnencryptedSuffix specified on the
 // Metadata struct, those not ending with EncryptedSuffix, if EncryptedSuffix
 // is provided (by default it is not), those not matching EncryptedRegex,
-// if EncryptedRegex is provided (by default it is not) or those matching
-// UnencryptedRegex, if UnencryptedRegex is provided (by default it is not).
+// if EncryptedRegex is provided (by default it is not), those matching UnencryptedRegex,
+// if UnencryptedRegex is provided (by default it is not), those with their comment
+// not matching EncryptedCommentRegex, if EncryptedCommentRegex is provided (by default
+// it is not), or those with their comment matching UnencryptedCommentRegex, if
+// UnencryptedCommentRegex is provided (by default it is not).
 // If encryption is successful, it returns the MAC for the encrypted tree
 // (all values if MACOnlyEncrypted is false, or only over values which end
 // up encrypted if MACOnlyEncrypted is true).
@@ -374,47 +478,12 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
 		hash.Write(MACOnlyEncryptedInitialization)
 	}
 	walk := func(branch TreeBranch) error {
-		_, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
-			encrypted := true
-			if tree.Metadata.UnencryptedSuffix != "" {
-				for _, v := range path {
-					if strings.HasSuffix(v, tree.Metadata.UnencryptedSuffix) {
-						encrypted = false
-						break
-					}
-				}
-			}
-			if tree.Metadata.EncryptedSuffix != "" {
-				encrypted = false
-				for _, v := range path {
-					if strings.HasSuffix(v, tree.Metadata.EncryptedSuffix) {
-						encrypted = true
-						break
-					}
-				}
-			}
-			if tree.Metadata.UnencryptedRegex != "" {
-				for _, p := range path {
-					matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p))
-					if matched {
-						encrypted = false
-						break
-					}
-				}
-			}
-			if tree.Metadata.EncryptedRegex != "" {
-				encrypted = false
-				for _, p := range path {
-					matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p))
-					if matched {
-						encrypted = true
-						break
-					}
-				}
-			}
+		_, err := branch.walkBranch(branch, make([]string, 0), make([][]string, 0), func(in interface{}, path []string, commentsStack [][]string) (interface{}, error) {
+			_, ok := in.(Comment)
+			encrypted := tree.shouldBeEncrypted(path, commentsStack, ok)
 			if !tree.Metadata.MACOnlyEncrypted || encrypted {
 				// Only add to MAC if not a comment
-				if _, ok := in.(Comment); !ok {
+				if !ok {
 					bytes, err := ToBytes(in)
 					if err != nil {
 						return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err)
@@ -429,6 +498,14 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
 				if err != nil {
 					return nil, fmt.Errorf("Could not encrypt value: %s", err)
 				}
+				if ok && tree.Metadata.UnencryptedCommentRegex != "" {
+					// If an encrypted comment matches tree.Metadata.UnencryptedCommentRegex, decryption will fail
+					// as the MAC does not match, and the commented value will not be decrypted.
+					matched, _ := regexp.Match(tree.Metadata.UnencryptedCommentRegex, []byte(in.(string)))
+					if matched {
+						return nil, fmt.Errorf("Encrypted comment %q matches UnencryptedCommentRegex! Make sure that UnencryptedCommentRegex cannot match an encrypted comment.", in)
+					}
+				}
 			}
 			return in, nil
 		})
@@ -464,49 +541,14 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
 		hash.Write(MACOnlyEncryptedInitialization)
 	}
 	walk := func(branch TreeBranch) error {
-		_, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
-			encrypted := true
-			if tree.Metadata.UnencryptedSuffix != "" {
-				for _, p := range path {
-					if strings.HasSuffix(p, tree.Metadata.UnencryptedSuffix) {
-						encrypted = false
-						break
-					}
-				}
-			}
-			if tree.Metadata.EncryptedSuffix != "" {
-				encrypted = false
-				for _, p := range path {
-					if strings.HasSuffix(p, tree.Metadata.EncryptedSuffix) {
-						encrypted = true
-						break
-					}
-				}
-			}
-			if tree.Metadata.UnencryptedRegex != "" {
-				for _, p := range path {
-					matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p))
-					if matched {
-						encrypted = false
-						break
-					}
-				}
-			}
-			if tree.Metadata.EncryptedRegex != "" {
-				encrypted = false
-				for _, p := range path {
-					matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p))
-					if matched {
-						encrypted = true
-						break
-					}
-				}
-			}
+		_, err := branch.walkBranch(branch, make([]string, 0), make([][]string, 0), func(in interface{}, path []string, commentsStack [][]string) (interface{}, error) {
+			c, ok := in.(Comment)
+			encrypted := tree.shouldBeEncrypted(path, commentsStack, ok)
 			var v interface{}
 			if encrypted {
 				var err error
 				pathString := strings.Join(path, ":") + ":"
-				if c, ok := in.(Comment); ok {
+				if ok {
 					v, err = cipher.Decrypt(c.Value, key, pathString)
 					if err != nil {
 						// Assume the comment was not encrypted in the first place
@@ -576,6 +618,8 @@ type Metadata struct {
 	EncryptedSuffix           string
 	UnencryptedRegex          string
 	EncryptedRegex            string
+	UnencryptedCommentRegex   string
+	EncryptedCommentRegex     string
 	MessageAuthenticationCode string
 	MACOnlyEncrypted          bool
 	Version                   string
diff --git a/stores/stores.go b/stores/stores.go
index 9ab308d14..169e8dbb5 100644
--- a/stores/stores.go
+++ b/stores/stores.go
@@ -56,6 +56,8 @@ type Metadata struct {
 	EncryptedSuffix           string      `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"`
 	UnencryptedRegex          string      `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"`
 	EncryptedRegex            string      `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"`
+	UnencryptedCommentRegex   string      `yaml:"unencrypted_comment_regex,omitempty" json:"unencrypted_comment_regex,omitempty"`
+	EncryptedCommentRegex     string      `yaml:"encrypted_comment_regex,omitempty" json:"encrypted_comment_regex,omitempty"`
 	MACOnlyEncrypted          bool        `yaml:"mac_only_encrypted,omitempty" json:"mac_only_encrypted,omitempty"`
 	Version                   string      `yaml:"version" json:"version"`
 }
@@ -119,6 +121,8 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
 	m.EncryptedSuffix = sopsMetadata.EncryptedSuffix
 	m.UnencryptedRegex = sopsMetadata.UnencryptedRegex
 	m.EncryptedRegex = sopsMetadata.EncryptedRegex
+	m.UnencryptedCommentRegex = sopsMetadata.UnencryptedCommentRegex
+	m.EncryptedCommentRegex = sopsMetadata.EncryptedCommentRegex
 	m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
 	m.MACOnlyEncrypted = sopsMetadata.MACOnlyEncrypted
 	m.Version = sopsMetadata.Version
@@ -260,9 +264,15 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
 	if m.EncryptedRegex != "" {
 		cryptRuleCount++
 	}
+	if m.UnencryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
+	if m.EncryptedCommentRegex != "" {
+		cryptRuleCount++
+	}
 
 	if cryptRuleCount > 1 {
-		return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex or unencrypted_regex in the same file")
+		return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file")
 	}
 
 	if cryptRuleCount == 0 {
@@ -277,6 +287,8 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
 		EncryptedSuffix:           m.EncryptedSuffix,
 		UnencryptedRegex:          m.UnencryptedRegex,
 		EncryptedRegex:            m.EncryptedRegex,
+		UnencryptedCommentRegex:   m.UnencryptedCommentRegex,
+		EncryptedCommentRegex:     m.EncryptedCommentRegex,
 		MACOnlyEncrypted:          m.MACOnlyEncrypted,
 		LastModified:              lastModified,
 	}, nil


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 8f4fb006b..857b50cb8 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -166,6 +166,22 @@ creation_rules:
     mac_only_encrypted: true
     `)
 
+var sampleConfigWithEncryptedCommentRegexParameters = []byte(`
+creation_rules:
+  - path_regex: barbar*
+    kms: "1"
+    pgp: "2"
+    encrypted_comment_regex: "sops:enc"
+    `)
+
+var sampleConfigWithUnencryptedCommentRegexParameters = []byte(`
+creation_rules:
+  - path_regex: barbar*
+    kms: "1"
+    pgp: "2"
+    unencrypted_comment_regex: "sops:dec"
+    `)
+
 var sampleConfigWithInvalidParameters = []byte(`
 creation_rules:
   - path_regex: foobar*
@@ -430,6 +446,18 @@ func TestLoadConfigFileWithMACOnlyEncrypted(t *testing.T) {
 	assert.Equal(t, true, conf.MACOnlyEncrypted)
 }
 
+func TestLoadConfigFileWithUnencryptedCommentRegex(t *testing.T) {
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithUnencryptedCommentRegexParameters, t), "/conf/path", "barbar", nil)
+	assert.Equal(t, nil, err)
+	assert.Equal(t, "sops:dec", conf.UnencryptedCommentRegex)
+}
+
+func TestLoadConfigFileWithEncryptedCommentRegex(t *testing.T) {
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEncryptedCommentRegexParameters, t), "/conf/path", "barbar", nil)
+	assert.Equal(t, nil, err)
+	assert.Equal(t, "sops:enc", conf.EncryptedCommentRegex)
+}
+
 func TestLoadConfigFileWithInvalidParameters(t *testing.T) {
 	_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil)
 	assert.NotNil(t, err)
diff --git a/sops_test.go b/sops_test.go
index 81ad23c99..4a58f0866 100644
--- a/sops_test.go
+++ b/sops_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"reflect"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -38,6 +39,23 @@ func (c reverseCipher) Decrypt(value string, key []byte, path string) (plaintext
 	return reverse(value), nil
 }
 
+type encPrefixCipher struct{}
+
+func (c encPrefixCipher) Encrypt(value interface{}, key []byte, path string) (string, error) {
+	b, err := ToBytes(value)
+	if err != nil {
+		return "", err
+	}
+	return "ENC:" + string(b), nil
+}
+func (c encPrefixCipher) Decrypt(value string, key []byte, path string) (plaintext interface{}, err error) {
+	v, ok := strings.CutPrefix(value, "ENC:")
+	if !ok {
+		return nil, fmt.Errorf("String not prefixed with 'ENC:'")
+	}
+	return v, nil
+}
+
 func TestUnencryptedSuffix(t *testing.T) {
 	branches := TreeBranches{
 		TreeBranch{
@@ -330,6 +348,295 @@ func TestMACOnlyEncryptedNoConfusion(t *testing.T) {
 	}
 }
 
+func TestEncryptedCommentRegex(t *testing.T) {
+	branches := TreeBranches{
+		TreeBranch{
+			TreeItem{
+				Key:   Comment{"sops:enc"},
+				Value: nil,
+			},
+			TreeItem{
+				Key:   "foo",
+				Value: "bar",
+			},
+			TreeItem{
+				Key: "bar",
+				Value: TreeBranch{
+					TreeItem{
+						Key:   "foo",
+						Value: "bar",
+					},
+					TreeItem{
+						Key:   Comment{"before"},
+						Value: nil,
+					},
+					TreeItem{
+						Key:   Comment{"sops:enc"},
+						Value: nil,
+					},
+					TreeItem{
+						Key:   "encrypted",
+						Value: "bar",
+					},
+				},
+			},
+			TreeItem{
+				Key: "array",
+				Value: []interface{}{
+					"bar",
+					Comment{"sops:enc"},
+					"baz",
+				},
+			},
+			TreeItem{
+				Key:   Comment{"sops:enc"},
+				Value: nil,
+			},
+			TreeItem{
+				Key:   Comment{"after"},
+				Value: nil,
+			},
+			TreeItem{
+				Key: "encarray",
+				Value: []interface{}{
+					"bar",
+					"baz",
+				},
+			},
+		},
+	}
+	tree := Tree{Branches: branches, Metadata: Metadata{EncryptedCommentRegex: "sops:enc"}}
+	expected := TreeBranch{
+		TreeItem{
+			Key:   Comment{"sops:enc"},
+			Value: nil,
+		},
+		TreeItem{
+			Key:   "foo",
+			Value: "rab",
+		},
+		TreeItem{
+			Key: "bar",
+			Value: TreeBranch{
+				TreeItem{
+					Key:   "foo",
+					Value: "bar",
+				},
+				TreeItem{
+					Key:   Comment{"before"},
+					Value: nil,
+				},
+				TreeItem{
+					Key:   Comment{"sops:enc"},
+					Value: nil,
+				},
+				TreeItem{
+					Key:   "encrypted",
+					Value: "rab",
+				},
+			},
+		},
+		TreeItem{
+			Key: "array",
+			Value: []interface{}{
+				"bar",
+				Comment{"sops:enc"},
+				"zab",
+			},
+		},
+		TreeItem{
+			Key:   Comment{"sops:enc"},
+			Value: nil,
+		},
+		TreeItem{
+			Key:   Comment{"retfa"},
+			Value: nil,
+		},
+		TreeItem{
+			Key: "encarray",
+			Value: []interface{}{
+				"rab",
+				"zab",
+			},
+		},
+	}
+	cipher := reverseCipher{}
+	_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
+	if err != nil {
+		t.Errorf("Encrypting the tree failed: %s", err)
+	}
+	if !reflect.DeepEqual(tree.Branches[0], expected) {
+		t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branches[0], expected)
+	}
+	_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher)
+	if err != nil {
+		t.Errorf("Decrypting the tree failed: %s", err)
+	}
+	expected[1].Value = "bar"
+	expected[2].Value.(TreeBranch)[3].Value = "bar"
+	expected[3].Value.([]interface{})[2] = "baz"
+	expected[5].Key = Comment{"after"}
+	expected[6].Value = []interface{}{
+		"bar",
+		"baz",
+	}
+	if !reflect.DeepEqual(tree.Branches[0], expected) {
+		t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected)
+	}
+}
+
+func TestUnencryptedCommentRegex(t *testing.T) {
+	branches := TreeBranches{
+		TreeBranch{
+			TreeItem{
+				Key:   Comment{"sops:noenc"},
+				Value: nil,
+			},
+			TreeItem{
+				Key:   "foo",
+				Value: "bar",
+			},
+			TreeItem{
+				Key: "bar",
+				Value: TreeBranch{
+					TreeItem{
+						Key:   "foo",
+						Value: "bar",
+					},
+					TreeItem{
+						Key:   Comment{"before"},
+						Value: nil,
+					},
+					TreeItem{
+						Key:   Comment{"sops:noenc"},
+						Value: nil,
+					},
+					TreeItem{
+						Key:   "notencrypted",
+						Value: "bar",
+					},
+				},
+			},
+			TreeItem{
+				Key: "array",
+				Value: []interface{}{
+					"bar",
+					Comment{"sops:noenc"},
+					"baz",
+				},
+			},
+			TreeItem{
+				Key:   Comment{"sops:noenc"},
+				Value: nil,
+			},
+			TreeItem{
+				Key:   Comment{"after"},
+				Value: nil,
+			},
+			TreeItem{
+				Key: "notencarray",
+				Value: []interface{}{
+					"bar",
+					"baz",
+				},
+			},
+		},
+	}
+	tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedCommentRegex: "sops:noenc"}}
+	expected := TreeBranch{
+		TreeItem{
+			Key:   Comment{"sops:noenc"},
+			Value: nil,
+		},
+		TreeItem{
+			Key:   "foo",
+			Value: "bar",
+		},
+		TreeItem{
+			Key: "bar",
+			Value: TreeBranch{
+				TreeItem{
+					Key:   "foo",
+					Value: "rab",
+				},
+				TreeItem{
+					Key:   Comment{"erofeb"},
+					Value: nil,
+				},
+				TreeItem{
+					Key:   Comment{"sops:noenc"},
+					Value: nil,
+				},
+				TreeItem{
+					Key:   "notencrypted",
+					Value: "bar",
+				},
+			},
+		},
+		TreeItem{
+			Key: "array",
+			Value: []interface{}{
+				"rab",
+				Comment{"sops:noenc"},
+				"baz",
+			},
+		},
+		TreeItem{
+			Key:   Comment{"sops:noenc"},
+			Value: nil,
+		},
+		TreeItem{
+			Key:   Comment{"after"},
+			Value: nil,
+		},
+		TreeItem{
+			Key: "notencarray",
+			Value: []interface{}{
+				"bar",
+				"baz",
+			},
+		},
+	}
+	cipher := reverseCipher{}
+	_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
+	if err != nil {
+		t.Errorf("Encrypting the tree failed: %s", err)
+	}
+	if !reflect.DeepEqual(tree.Branches[0], expected) {
+		t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branches[0], expected)
+	}
+	_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher)
+	if err != nil {
+		t.Errorf("Decrypting the tree failed: %s", err)
+	}
+	expected[2].Value.(TreeBranch)[0].Value = "bar"
+	expected[2].Value.(TreeBranch)[1].Key = Comment{"before"}
+	expected[3].Value.([]interface{})[0] = "bar"
+	if !reflect.DeepEqual(tree.Branches[0], expected) {
+		t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected)
+	}
+}
+
+func TestUnencryptedCommentRegexFail(t *testing.T) {
+	branches := TreeBranches{
+		TreeBranch{
+			TreeItem{
+				Key:   Comment{"sops:noenc"},
+				Value: nil,
+			},
+			TreeItem{
+				Key:   "foo",
+				Value: "bar",
+			},
+		},
+	}
+	tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedCommentRegex: "ENC"}}
+	cipher := encPrefixCipher{}
+	_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
+	assert.NotNil(t, err)
+	assert.Contains(t, err.Error(), "Encrypted comment \"ENC:sops:noenc\" matches UnencryptedCommentRegex!")
+}
+
 type MockCipher struct{}
 
 func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) {