timoni icon indicating copy to clipboard operation
timoni copied to clipboard

[proposal] Encryption of Timoni values

Open phoban01 opened this issue 1 year ago • 5 comments

Context

The following is a proposal for supporting the encryption of Timoni values (based on prior art, see: https://github.com/phoban01/cue-sops).

The proposal was first mooted in #71.

Proposal

Timoni enables the encryption of sensitive values via built-in SOPS support.

Given the following config definition:

#Config: {
  api: {
    url:  "https//api.github.com/user"
    token: string
  }
}

A values file providing an API key is marked as sensitive using the @secret() annotation:

# api-values.cue
values: {
    api: token: "gh_personalaccesstoken" @secret()
}

The plaintext value can then be encrypted in-place:

timoni encrypt -f api-values.cue

This will produce the following result:

# api-values.cue
values: {
	api: token: "ENC[AES256_GCM,data:0SeH+BIX6SwJBsgwLmDOJHU7,iv:Fx1bpRKrz4wKztuEXMfa0KuRqLcOu9ZLT8OYdH+i58c=,tag:IoDhNZpGnGhqmDllgUVdUg==,type:str]" @secret()
}

// DO NOT EDIT: auto-generated by timoni
sops: {
	kms:      null
	gcp_kms:  null
	azure_kv: null
	hc_vault: null
	age: [{
		recipient: "age1ethasxep4zkax64yfx35rn2t4yeul4254w764l9gtasvn2rwpv7s733dq7"
		enc: """
			-----BEGIN AGE ENCRYPTED FILE-----
			YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQMk43bjFuUytFNTlIclNW
			Y1RnNWEwc2FGOUd4VW5NODNwdEVKOXlJd2k4ClNubDN0Qktuck5IVnN6ZjZBOTEz
			OFhNRUc3aUs1Y09DQTF6OTlWRU9ZQ00KLS0tIGdGTUlZWUJyVkZKZXdvMzZhV294
			c0E5bHVkSHc0MkhFUnhiODFlbzV5SE0KlNEhfwHl/VDZzfkpGb2/s7KbTFRA4U/K
			u5OM5P2YTvpSkmVbdVLLcX7eFHVyLZOukarFXEZ65rq9baMO0lJ3Vg==
			-----END AGE ENCRYPTED FILE-----

			"""
	}]
	lastmodified:    "2023-04-01T12:00:00Z"
	mac:             "ENC[AES256_GCM,data:heUT68PAirogTfcV+4pR8RNjx+d3cEE+Zn5e97xNy2wJvwZ4ecxnxItDj60E71aTK80UxCxkWkfjg2ZGKscPCMKoAXBkli6y/ab0e0+9uulvqjbd51m7mzGo/DMt65Ab7C6hq6S/VuI9JvvR7OVdgpvrliQzlCx2VENYNG6/r/0=,iv:gPvKgisLoTuOEIMNQgwY3zhPUEDkjJrRTyGWEEMr1ww=,tag:P8OlN/XDfWZqo6ZIchwbzw==,type:str]"
	pgp:             null
	encrypted_regex: "token|SECRET"
	version:         "3.7.3"
}

Timoni will decrypt values before applying an instance to the cluster:

timoni -n default apply gh-app \
  oci://ghcr.io/phoban01/modules/gh-app \
  --values api-values.cue

Timoni can also decrypt a file in-place:

timoni decrypt -f api-values.cue

Encrypt multiple fields

To encrypt all fields in a file (optionally matching field names via regex), it is possible to use a global annotation:

# api-values.cue
@secret(include="[regex-pattern]", exclude="[regex-pattern]")

values: {
    api: token: "gh_personalaccesstoken"
}

SOPS configuration

It is possible to specify the encryption service used by SOPs per field:

# api-values.cue
values: {
    age: "supersecret" @secret(age="age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw")
    pgp: "supersecret" @secret(pgp_fp="85D77543B3D624B63CEA9E6DBC17301B491B3F21")
    aws: "supersecret" @secret(aws_kms="arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e")
    aws: "supersecret" @secret(aws_encryption_context="Environment:production,Role:web-server")
    gcp: "supersecret" @secret(gcp_kms="projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key")
    azure: "supersecret" @secret(azure_kv="https://sops.vault.azure.net/keys/sops-key/some-string")
    hashicorp_vault: "supersecret" @secret(hc_vault_transit="https://vault-server:8200/v1/sops/keys/firstkey")
}

It is also possible to define encryption service providers globally using the @sops() annotation. Providers can subsequently be referenced using labels:

# api-values.cue
@sops(label="aws-master", aws_kms="arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e")
@sops(label="hashi-vault", hc_vault_transit="https://vault-server:8200/v1/sops/keys/firstkey")

values: {
    aws: "supersecret" @secret(label="aws-master")
    hashicorp_vault: "supersecret" @secret(label"aws-master")
}

If a single @sops() annotation is present then labels can be omitted and the specified service will be used to encrypt all values:

# api-values.cue
@sops(aws_kms="arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e")

values: {
    aws: "supersecret" @secret()
    hashicorp_vault: "supersecret" @secret()
}

Bundles

Values provided in a Bundle may also be encrypted. Encryption services can be defined as part of the bundle spec and then referenced by name in the @secret annotation.

#Bundle: {
    apiVersion: string
    encryption: [{
      name: string
      type: age | pgp_fp | aws_kms | aws_encryption_context | gcp_kms | azure_kv | hc_vault_transit
      value: string
    }]
    instances: [string]: {
        module: {
            url:     string
            digest?: string
            version: *"latest" | string
        }
        namespace: string
        values: {
          api_key: "123457890abcdefg" @secret(name=string)
        }
    }
}

To encrypt a bundle use the bundle encrypt subcommand:

timoni bundle encrypt -f bundle.cue

Sensitive values will be automatically decrypted when the bundle is applied:

timoni bundle apply -f bundle.cue

Timoni bundle diffs will decrypt both previous and current sensitive values and display the diff in cleartext:

timoni bundle apply --dry-run --diff -f bundle.cue

phoban01 avatar Apr 12 '23 11:04 phoban01