Skip to content

Commit 9a2c576

Browse files
Marvin ImmickMarvin Immick
authored andcommitted
feat: add T Cloud Public KMS support
1 parent d4bcce9 commit 9a2c576

13 files changed

Lines changed: 909 additions & 157 deletions

File tree

README.rst

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ SOPS: Secrets OPerationS
22
========================
33

44
**SOPS** is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY
5-
formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, age, and PGP.
5+
formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, T Cloud Public, age, and PGP.
66
(`demo <https://www.youtube.com/watch?v=YTEVyLXFiq0>`_)
77

88
.. image:: https://i.imgur.com/X0TM5NI.gif
@@ -604,13 +604,57 @@ You can also configure HuaweiCloud KMS keys in the ``.sops.yaml`` config file:
604604
hckms:
605605
- tr-west-1:abc12345-6789-0123-4567-890123456789,tr-west-2:def67890-1234-5678-9012-345678901234
606606
607+
Encrypting using T Cloud Public KMS
608+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
609+
610+
The T Cloud Public (formerly OpenTelekomCloud) KMS integration uses the
611+
`AuthenticatedClient function <https://github.com/opentelekomcloud/gophertelekomcloud/openstack/client.go>`_
612+
which expects the environment variables ``OS_ACCESS_KEY``, ``OS_SECRET_KEY``, ``OS_AUTH_URL`` to be set:
613+
614+
.. code:: bash
615+
616+
export OS_ACCESS_KEY="your-access-key"
617+
export OS_SECRET_KEY="your-secret-key"
618+
export OS_AUTH_URL="https://iam.eu-de.otc.t-systems.com/v3"
619+
620+
Encrypting/decrypting with T Cloud Public KMS requires a KMS key ID. You can get the key ID from the T Cloud Public console or using
621+
the T Cloud Public API.
622+
623+
Now you can encrypt a file using:
624+
625+
.. code:: sh
626+
627+
$ sops encrypt --tcloudkms tr-west-1:ghi12345-6789-0123-4567-890123456789 test.yaml > test.enc.yaml
628+
629+
Or using the environment variable:
630+
631+
.. code:: sh
632+
633+
$ export SOPS_TCLOUD_KMS_IDS="ghi12345-6789-0123-4567-890123456789"
634+
$ sops encrypt test.yaml > test.enc.yaml
635+
636+
And decrypt it using:
637+
638+
.. code:: sh
639+
640+
$ sops decrypt test.enc.yaml
641+
642+
You can also configure T Cloud Public KMS keys in the ``.sops.yaml`` config file:
643+
644+
.. code:: yaml
645+
646+
creation_rules:
647+
- path_regex: \.tcloudkms\.yaml$
648+
tcloudkms:
649+
- ghi12345-6789-0123-4567-890123456789,jkl67890-1234-5678-9012-345678901234
650+
607651
Adding and removing keys
608652
~~~~~~~~~~~~~~~~~~~~~~~~
609653
610654
When creating new files, ``sops`` uses the PGP, KMS and GCP KMS defined in the
611-
command line arguments ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms`` or ``--azure-kv``, or from
655+
command line arguments ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms``, ``--azure-kv`` or ``--tcloudkms``, or from
612656
the environment variables ``SOPS_KMS_ARN``, ``SOPS_PGP_FP``, ``SOPS_GCP_KMS_IDS``,
613-
``SOPS_HUAWEICLOUD_KMS_IDS``, ``SOPS_AZURE_KEYVAULT_URLS``. That information is stored in the file under the
657+
``SOPS_HUAWEICLOUD_KMS_IDS``, ``SOPS_AZURE_KEYVAULT_URLS``, ``SOPS_TCLOUD_KMS_IDS``. That information is stored in the file under the
614658
``sops`` section, such that decrypting files does not require providing those
615659
parameters again.
616660
@@ -654,9 +698,9 @@ disabled by supplying the ``-y`` flag.
654698
655699
The ``rotate`` command generates a new data encryption key and reencrypt all values
656700
with the new key. At the same time, the command line flag ``--add-kms``, ``--add-pgp``,
657-
``--add-gcp-kms``, ``--add-hckms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms``,
658-
``--rm-hckms`` and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use
659-
the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms`` and ``--azure-kv``
701+
``--add-gcp-kms``, ``--add-hckms``, ``--add-azure-kv``, ``--add-tcloudkms``, --rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms``,
702+
``--rm-hckms``, ``--rm-azure-kv`` and ``--rm-tcloudkms`` can be used to add and remove keys from a file. These flags use
703+
the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms``, ``--azure-kv`` and ``--tcloudkms``
660704
arguments when creating new files.
661705
662706
Use ``updatekeys`` if you want to add a key without rotating the data key.
@@ -878,6 +922,10 @@ can manage the three sets of configurations for the three types of files:
878922
- path_regex: \.hckms\.yaml$
879923
hckms: tr-west-1:abc12345-6789-0123-4567-890123456789,tr-west-2:def67890-1234-5678-9012-345678901234
880924
925+
# tcloudkms files using T Cloud Public KMS
926+
- path_regex: \.tcloudkms\.yaml$
927+
tcloudkms: ghi12345-6789-0123-4567-890123456789,jkl67890-1234-5678-9012-345678901234
928+
881929
# Finally, if the rules above have not matched, this one is a
882930
# catchall that will encrypt the file using KMS set C as well as PGP
883931
# The absence of a path_regex means it will match everything
@@ -1883,6 +1931,16 @@ To directly specify a single key group, you can use the following keys:
18831931
- tr-west-1:abc12345-6789-0123-4567-890123456789
18841932
- tr-west-1:def67890-1234-5678-9012-345678901234
18851933
1934+
* ``tcloudkms`` (list of strings): list of T Cloud Public KMS key IDs (format: ``<key-uuid>``).
1935+
Example:
1936+
1937+
.. code:: yaml
1938+
1939+
creation_rules:
1940+
- tcloudkms:
1941+
- ghi12345-6789-0123-4567-890123456789
1942+
- jkl67890-1234-5678-9012-345678901234
1943+
18861944
To specify a list of key groups, you can use the following key:
18871945
18881946
* ``key_groups`` (list of key group objects): a list of key group objects.
@@ -1912,6 +1970,8 @@ To specify a list of key groups, you can use the following key:
19121970
- http://my.vault/v1/sops/keys/secondkey
19131971
hckms:
19141972
- tr-west-1:abc12345-6789-0123-4567-890123456789
1973+
tcloudkms:
1974+
- ghi12345-6789-0123-4567-890123456789
19151975
19161976
merge:
19171977
- pgp:
@@ -2000,6 +2060,17 @@ A key group supports the following keys:
20002060
20012061
- key_id: tr-west-1:abc12345-6789-0123-4567-890123456789
20022062
2063+
* ``tcloudkms`` (list of objects): list of T Cloud Public KMS key IDs.
2064+
Every object must have the following key:
2065+
2066+
* ``key_id`` (string): the key ID in format ``<key-uuid>``.
2067+
2068+
Example:
2069+
2070+
.. code:: yaml
2071+
2072+
- key_id: ghi12345-6789-0123-4567-890123456789
2073+
20032074
* ``age`` (list of strings): list of Age public keys.
20042075
20052076
* ``pgp`` (list of strings): list of PGP/GPG key fingerprints.

cmd/sops/main.go

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/getsops/sops/v3/stores"
4545
"github.com/getsops/sops/v3/stores/dotenv"
4646
"github.com/getsops/sops/v3/stores/json"
47+
"github.com/getsops/sops/v3/tcloudkms"
4748
"github.com/getsops/sops/v3/version"
4849
)
4950

@@ -91,13 +92,13 @@ func main() {
9192
},
9293
}
9394
app.Name = "sops"
94-
app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, Azure Key Vault, age, and GPG support"
95+
app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, T Cloud Public KMS, Azure Key Vault, age, and GPG support"
9596
app.ArgsUsage = "sops [options] file"
9697
app.Version = version.Version
9798
app.Authors = []cli.Author{
9899
{Name: "CNCF Maintainers"},
99100
}
100-
app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, AZKV,
101+
app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, T Cloud Public KMS, AZKV,
101102
PGP, and Age
102103
103104
To encrypt or decrypt a document with AWS KMS, specify the KMS ARN
@@ -117,6 +118,12 @@ func main() {
117118
HUAWEICLOUD_SDK_AK, HUAWEICLOUD_SDK_SK, HUAWEICLOUD_SDK_PROJECT_ID, or
118119
use credentials file at ~/.huaweicloud/credentials)
119120
121+
To encrypt or decrypt a document with T Cloud Public KMS, specify the
122+
T Cloud Public KMS key ID (format: key-uuid) in the --tcloudkms flag or in the
123+
OS_KMS_IDS environment variable.
124+
(You need to setup T Cloud Public credentials via environment variables:
125+
OS_ACCESS_KEY, OS_SECRET_KEY, OS_AUTH_URL)
126+
120127
To encrypt or decrypt a document with HashiCorp Vault's Transit Secret
121128
Engine, specify the Vault key URI name in the --hc-vault-transit flag
122129
or in the SOPS_VAULT_URIS environment variable (for example
@@ -180,7 +187,8 @@ func main() {
180187
fmt.Fprint(c.App.Writer, GenZshCompletion(app.Name))
181188
return nil
182189
},
183-
}},
190+
},
191+
},
184192
},
185193
{
186194
Name: "exec-env",
@@ -582,6 +590,10 @@ func main() {
582590
Name: "hckms",
583591
Usage: "the HuaweiCloud KMS key ID (format: region:key-uuid) the new group should contain. Can be specified more than once",
584592
},
593+
cli.StringSliceFlag{
594+
Name: "tcloudkms",
595+
Usage: "the T Cloud Public KMS key ID (format: key-uuid) the new group should contain. Can be specified more than once",
596+
},
585597
cli.StringSliceFlag{
586598
Name: "azure-kv",
587599
Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once",
@@ -950,6 +962,11 @@ func main() {
950962
Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)",
951963
EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS",
952964
},
965+
cli.StringFlag{
966+
Name: "tcloudkms",
967+
Usage: "comma separated list of T Cloud Public KMS key IDs (format: key-uuid)",
968+
EnvVar: "SOPS_TCLOUD_KMS_IDS",
969+
},
953970
cli.StringFlag{
954971
Name: "azure-kv",
955972
Usage: "comma separated list of Azure Key Vault URLs",
@@ -1068,7 +1085,6 @@ func main() {
10681085
KeyServices: svcs,
10691086
encryptConfig: encConfig,
10701087
})
1071-
10721088
if err != nil {
10731089
return toExitError(err)
10741090
}
@@ -1143,6 +1159,14 @@ func main() {
11431159
Name: "rm-hckms",
11441160
Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file",
11451161
},
1162+
cli.StringFlag{
1163+
Name: "add-tcloudkms",
1164+
Usage: "add the provided comma-separated list of T Cloud Public KMS key IDs (format: key-uuid) to the list of master keys on the given file",
1165+
},
1166+
cli.StringFlag{
1167+
Name: "rm-tcloudkms",
1168+
Usage: "remove the provided comma-separated list of T Cloud Public KMS key IDs (format: key-uuid) from the list of master keys on the given file",
1169+
},
11461170
cli.StringFlag{
11471171
Name: "add-azure-kv",
11481172
Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
@@ -1209,8 +1233,8 @@ func main() {
12091233
return toExitError(err)
12101234
}
12111235
if _, err := os.Stat(fileName); os.IsNotExist(err) {
1212-
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
1213-
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
1236+
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-tcloudkms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
1237+
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-tcloudkms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
12141238
return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile)
12151239
}
12161240
}
@@ -1301,6 +1325,11 @@ func main() {
13011325
Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)",
13021326
EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS",
13031327
},
1328+
cli.StringFlag{
1329+
Name: "tcloudkms",
1330+
Usage: "comma separated list of T Cloud Public KMS key IDs (format: key-uuid)",
1331+
EnvVar: "SOPS_TCLOUD_KMS_IDS",
1332+
},
13041333
cli.StringFlag{
13051334
Name: "azure-kv",
13061335
Usage: "comma separated list of Azure Key Vault URLs",
@@ -1714,6 +1743,11 @@ func main() {
17141743
Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)",
17151744
EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS",
17161745
},
1746+
cli.StringFlag{
1747+
Name: "tcloudkms",
1748+
Usage: "comma separated list of T Cloud Public KMS key IDs (format: key-uuid)",
1749+
EnvVar: "SOPS_TCLOUD_KMS_IDS",
1750+
},
17171751
cli.StringFlag{
17181752
Name: "azure-kv",
17191753
Usage: "comma separated list of Azure Key Vault URLs",
@@ -1770,6 +1804,14 @@ func main() {
17701804
Name: "rm-hckms",
17711805
Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file",
17721806
},
1807+
cli.StringFlag{
1808+
Name: "add-tcloudkms",
1809+
Usage: "add the provided comma-separated list of T Cloud Public KMS key IDs (format: key-uuid) to the list of master keys on the given file",
1810+
},
1811+
cli.StringFlag{
1812+
Name: "rm-tcloudkms",
1813+
Usage: "remove the provided comma-separated list of T Cloud Public KMS key IDs (format: key-uuid) from the list of master keys on the given file",
1814+
},
17731815
cli.StringFlag{
17741816
Name: "add-azure-kv",
17751817
Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
@@ -1904,8 +1946,8 @@ func main() {
19041946
return toExitError(err)
19051947
}
19061948
if _, err := os.Stat(fileName); os.IsNotExist(err) {
1907-
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
1908-
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
1949+
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-tcloudkms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
1950+
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-tcloudkms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
19091951
return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile)
19101952
}
19111953
if isEncryptMode || isDecryptMode || isRotateMode {
@@ -2235,7 +2277,7 @@ func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store,
22352277
}, nil
22362278
}
22372279

2238-
func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) {
2280+
func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, tcloudkmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) {
22392281
var masterKeys []keys.MasterKey
22402282
for _, k := range kms.MasterKeysFromArnString(c.String(kmsOptionName), kmsEncryptionContext, c.String("aws-profile")) {
22412283
masterKeys = append(masterKeys, k)
@@ -2253,6 +2295,13 @@ func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsO
22532295
for _, k := range hckmsKeys {
22542296
masterKeys = append(masterKeys, k)
22552297
}
2298+
tcloudkmsKeys, err := tcloudkms.NewMasterKeysFromKeyIDString(c.String(tcloudkmsOptionName))
2299+
if err != nil {
2300+
return nil, err
2301+
}
2302+
for _, k := range tcloudkmsKeys {
2303+
masterKeys = append(masterKeys, k)
2304+
}
22562305
azureKeys, err := azkv.MasterKeysFromURLs(c.String(azureKvOptionName))
22572306
if err != nil {
22582307
return nil, err
@@ -2279,11 +2328,11 @@ func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsO
22792328

22802329
func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) {
22812330
kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
2282-
addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-hckms", "add-azure-kv", "add-hc-vault-transit", "add-age")
2331+
addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-hckms", "add-tcloudkms", "add-azure-kv", "add-hc-vault-transit", "add-age")
22832332
if err != nil {
22842333
return rotateOpts{}, err
22852334
}
2286-
rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-hckms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age")
2335+
rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-hckms", "rm-tcloudkms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age")
22872336
if err != nil {
22882337
return rotateOpts{}, err
22892338
}
@@ -2432,6 +2481,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so
24322481
var azkvKeys []keys.MasterKey
24332482
var hcVaultMkKeys []keys.MasterKey
24342483
var hckmsMkKeys []keys.MasterKey
2484+
var tcloudkmsMkKeys []keys.MasterKey
24352485
var ageMasterKeys []keys.MasterKey
24362486
kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
24372487
if c.String("encryption-context") != "" && kmsEncryptionContext == nil {
@@ -2456,6 +2506,15 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so
24562506
hckmsMkKeys = append(hckmsMkKeys, k)
24572507
}
24582508
}
2509+
if c.String("tcloudkms") != "" {
2510+
tcloudkmsKeys, err := tcloudkms.NewMasterKeysFromKeyIDString(c.String("tcloudkms"))
2511+
if err != nil {
2512+
return nil, err
2513+
}
2514+
for _, k := range tcloudkmsKeys {
2515+
tcloudkmsMkKeys = append(tcloudkmsMkKeys, k)
2516+
}
2517+
}
24592518
if c.String("azure-kv") != "" {
24602519
azureKeys, err := azkv.MasterKeysFromURLs(c.String("azure-kv"))
24612520
if err != nil {
@@ -2488,7 +2547,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so
24882547
ageMasterKeys = append(ageMasterKeys, k)
24892548
}
24902549
}
2491-
if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("hckms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" {
2550+
if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("hckms") == "" && c.String("tcloudkms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" {
24922551
conf := optionalConfig
24932552
var err error
24942553
if conf == nil {
@@ -2508,6 +2567,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so
25082567
group = append(group, kmsKeys...)
25092568
group = append(group, cloudKmsKeys...)
25102569
group = append(group, hckmsMkKeys...)
2570+
group = append(group, tcloudkmsMkKeys...)
25112571
group = append(group, azkvKeys...)
25122572
group = append(group, pgpKeys...)
25132573
group = append(group, hcVaultMkKeys...)

0 commit comments

Comments
 (0)