Skip to content

Commit f58bc35

Browse files
committed
feat: add support for Alibaba Cloud KMS in keyservice
1 parent 09cf07a commit f58bc35

14 files changed

Lines changed: 993 additions & 132 deletions

File tree

README.rst

Lines changed: 66 additions & 7 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, Alibaba Cloud KMS, HuaweiCloud KMS, age, and PGP.
66
(`demo <https://www.youtube.com/watch?v=YTEVyLXFiq0>`_)
77

88
.. image:: https://i.imgur.com/X0TM5NI.gif
@@ -597,13 +597,52 @@ You can also configure HuaweiCloud KMS keys in the ``.sops.yaml`` config file:
597597
hckms:
598598
- tr-west-1:abc12345-6789-0123-4567-890123456789,tr-west-2:def67890-1234-5678-9012-345678901234
599599
600+
Encrypting using Alibaba Cloud KMS
601+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
602+
603+
The Alibaba Cloud KMS integration uses the Alibaba Cloud SDK to communicate with the KMS service.
604+
It supports authentication via:
605+
606+
1. **Environment Variables**: ``ALIBABA_CLOUD_ACCESS_KEY_ID`` and ``ALIBABA_CLOUD_ACCESS_KEY_SECRET``.
607+
2. **CLI Configuration**: It can read credentials from the Alibaba Cloud CLI configuration (``~/.aliyun/config.json``).
608+
3. **Instance RAM Roles**: When running on an ECS instance with an attached RAM role.
609+
610+
For example, using environment variables:
611+
612+
.. code:: bash
613+
614+
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
615+
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
616+
617+
To encrypt a file, specify the Alibaba Cloud KMS key using its ARN format:
618+
``acs:kms:RegionId:UserId:key/CmkId``.
619+
620+
.. code:: sh
621+
622+
$ sops encrypt --acs-kms acs:kms:cn-shanghai:1234567890:key/key-idxxxx test.yaml > test.enc.yaml
623+
624+
Or using the ``SOPS_ACS_KMS_IDS`` environment variable:
625+
626+
.. code:: bash
627+
628+
export SOPS_ACS_KMS_IDS="acs:kms:cn-shanghai:1234567890:key/key-idxxxx"
629+
$ sops encrypt test.yaml > test.enc.yaml
630+
631+
You can also configure Alibaba Cloud KMS keys in the ``.sops.yaml`` config file:
632+
633+
.. code:: yaml
634+
635+
creation_rules:
636+
- path_regex: \.acs\.yaml$
637+
acs_kms: "acs:kms:cn-shanghai:1234567890:key/key-idxxxx"
638+
600639
Adding and removing keys
601640
~~~~~~~~~~~~~~~~~~~~~~~~
602641
603642
When creating new files, ``sops`` uses the PGP, KMS and GCP KMS defined in the
604-
command line arguments ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms`` or ``--azure-kv``, or from
643+
command line arguments ``--kms``, ``--pgp``, ``--gcp-kms``, ``--acs-kms``, ``--hckms`` or ``--azure-kv``, or from
605644
the environment variables ``SOPS_KMS_ARN``, ``SOPS_PGP_FP``, ``SOPS_GCP_KMS_IDS``,
606-
``SOPS_HUAWEICLOUD_KMS_IDS``, ``SOPS_AZURE_KEYVAULT_URLS``. That information is stored in the file under the
645+
``SOPS_ACS_KMS_IDS``, ``SOPS_HUAWEICLOUD_KMS_IDS``, ``SOPS_AZURE_KEYVAULT_URLS``. That information is stored in the file under the
607646
``sops`` section, such that decrypting files does not require providing those
608647
parameters again.
609648
@@ -647,9 +686,9 @@ disabled by supplying the ``-y`` flag.
647686
648687
The ``rotate`` command generates a new data encryption key and reencrypt all values
649688
with the new key. At the same time, the command line flag ``--add-kms``, ``--add-pgp``,
650-
``--add-gcp-kms``, ``--add-hckms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms``,
651-
``--rm-hckms`` and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use
652-
the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms``, ``--hckms`` and ``--azure-kv``
689+
``--add-gcp-kms``, ``--add-acs-kms``, ``--add-hckms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms``,
690+
``--rm-acs-kms``, ``--rm-hckms`` and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use
691+
the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms``, ``--acs-kms``, ``--hckms`` and ``--azure-kv``
653692
arguments when creating new files.
654693
655694
Use ``updatekeys`` if you want to add a key without rotating the data key.
@@ -825,7 +864,7 @@ stdout.
825864
Using .sops.yaml conf to select KMS, PGP and age for new files
826865
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
827866
828-
It is often tedious to specify the ``--kms`` ``--gcp-kms`` ``--hckms`` ``--pgp`` and ``--age`` parameters for creation
867+
It is often tedious to specify the ``--kms`` ``--gcp-kms`` ``--acs-kms`` ``--hckms`` ``--pgp`` and ``--age`` parameters for creation
829868
of all new files. If your secrets are stored under a specific directory, like a
830869
``git`` repository, you can create a ``.sops.yaml`` configuration file at the root
831870
directory to define which keys are used for which filename.
@@ -1866,6 +1905,15 @@ To directly specify a single key group, you can use the following keys:
18661905
- hc_vault_transit_uri:
18671906
- http://my.vault/v1/sops/keys/secondkey
18681907
1908+
* ``acs_kms`` (list of strings): list of Alibaba Cloud KMS key ARNs.
1909+
Example:
1910+
1911+
.. code:: yaml
1912+
1913+
creation_rules:
1914+
- acs_kms:
1915+
- acs:kms:cn-shanghai:1234567890:key/key-idxxxx
1916+
18691917
* ``hckms`` (list of strings): list of HuaweiCloud KMS key IDs (format: ``<region>:<key-uuid>``).
18701918
Example:
18711919
@@ -1982,6 +2030,17 @@ A key group supports the following keys:
19822030
19832031
* ``hc_vault`` (list of strings): list of HashiCorp Vault transit URIs.
19842032
2033+
* ``acs_kms`` (list of objects): list of Alibaba Cloud KMS key ARNs.
2034+
Every object must have the following key:
2035+
2036+
* ``arn`` (string): the key ARN.
2037+
2038+
Example:
2039+
2040+
.. code:: yaml
2041+
2042+
- arn: acs:kms:cn-shanghai:1234567890:key/key-idxxxx
2043+
19852044
* ``hckms`` (list of objects): list of HuaweiCloud KMS key IDs.
19862045
Every object must have the following key:
19872046

acskms/keysource.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package acskms
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"os"
7+
"regexp"
8+
"strings"
9+
"time"
10+
11+
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
12+
kmssdk "github.com/alibabacloud-go/kms-20160120/v3/client"
13+
"github.com/alibabacloud-go/tea/tea"
14+
"github.com/aliyun/credentials-go/credentials"
15+
"github.com/getsops/sops/v3/keys"
16+
"github.com/getsops/sops/v3/logging"
17+
"github.com/sirupsen/logrus"
18+
)
19+
20+
const (
21+
// arnRegex matches an ACS ARN, for example:
22+
// "acs:kms:cn-shanghai:1234567890:key/key-idxxxxx".
23+
arnRegex = `^acs:kms:(.+):[0-9]+:key/(.+)$`
24+
// kmsTTL is the duration after which a MasterKey requires rotation.
25+
kmsTTL = time.Hour
26+
// KeyTypeIdentifier is the string used to identify an ACS KMS MasterKey.
27+
KeyTypeIdentifier = "acs_kms"
28+
)
29+
30+
var (
31+
// log is the global logger for any Alibaba Cloud KMS MasterKey.
32+
log *logrus.Logger
33+
)
34+
35+
func init() {
36+
log = logging.NewLogger("ACSKMS")
37+
}
38+
39+
// MasterKey is an Alibaba Cloud KMS key used to encrypt and decrypt SOPS' data key.
40+
type MasterKey struct {
41+
// Arn is the full key identifier in format "region:key-id" or an ARN
42+
Arn string
43+
// Region is the Alibaba Cloud region (e.g., "cn-hangzhou")
44+
Region string
45+
// EncryptedKey stores the data key in its encrypted form.
46+
EncryptedKey string
47+
// CreationDate is when this MasterKey was created.
48+
CreationDate time.Time
49+
}
50+
51+
// NewMasterKey creates a new MasterKey from a key arn string, setting
52+
// the creation date to the current date.
53+
func NewMasterKey(arn string) (*MasterKey, error) {
54+
region, err := parseKeyArn(arn)
55+
if err != nil {
56+
return nil, err
57+
}
58+
return &MasterKey{
59+
Arn: arn,
60+
Region: region,
61+
CreationDate: time.Now().UTC(),
62+
}, nil
63+
}
64+
65+
// NewMasterKeyFromKeyIDString takes a comma separated list of Alibaba Cloud KMS
66+
// key ARNs, and returns a slice of new MasterKeys.
67+
func NewMasterKeyFromKeyIDString(keyArn string) ([]*MasterKey, error) {
68+
var keys []*MasterKey
69+
if keyArn == "" {
70+
return keys, nil
71+
}
72+
for _, s := range strings.Split(keyArn, ",") {
73+
s = strings.TrimSpace(s)
74+
if s == "" {
75+
continue
76+
}
77+
k, err := NewMasterKey(s)
78+
if err != nil {
79+
return nil, err
80+
}
81+
keys = append(keys, k)
82+
}
83+
return keys, nil
84+
}
85+
86+
// parseKeyArn parse an Alibaba Cloud KMS key identifier, which can be a full ARN.
87+
func parseKeyArn(arn string) (string, error) {
88+
re := regexp.MustCompile(arnRegex)
89+
matches := re.FindStringSubmatch(arn)
90+
if len(matches) != 3 {
91+
return "", fmt.Errorf("invalid ACS KMS key ARN: %s", arn)
92+
}
93+
94+
return matches[1], nil
95+
}
96+
97+
// Encrypt encrypts the data key using Alibaba Cloud KMS.
98+
func (key *MasterKey) Encrypt(dataKey []byte) error {
99+
client, err := key.getClient()
100+
if err != nil {
101+
return err
102+
}
103+
104+
request := &kmssdk.EncryptRequest{
105+
KeyId: tea.String(key.Arn),
106+
Plaintext: tea.String(base64.StdEncoding.EncodeToString(dataKey)),
107+
}
108+
109+
resp, err := client.Encrypt(request)
110+
if err != nil {
111+
return fmt.Errorf("acskms encrypt error: %v", err)
112+
}
113+
114+
key.EncryptedKey = *resp.Body.CiphertextBlob
115+
return nil
116+
}
117+
118+
// Decrypt decrypts the data key using Alibaba Cloud KMS.
119+
func (key *MasterKey) Decrypt() ([]byte, error) {
120+
client, err := key.getClient()
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
request := &kmssdk.DecryptRequest{
126+
CiphertextBlob: tea.String(key.EncryptedKey),
127+
}
128+
// If an endpoint is manually set (e.g. KMS Instance), we might need to rely on the SDK's behavior.
129+
// The standard SDK usually works fine with CiphertextBlob.
130+
131+
resp, err := client.Decrypt(request)
132+
if err != nil {
133+
return nil, fmt.Errorf("acskms decrypt error: %v", err)
134+
}
135+
136+
return base64.StdEncoding.DecodeString(*resp.Body.Plaintext)
137+
}
138+
139+
// EncryptIfNeeded encrypts the data key if it's not already encrypted.
140+
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
141+
if key.EncryptedKey == "" {
142+
return key.Encrypt(dataKey)
143+
}
144+
return nil
145+
}
146+
147+
// EncryptedDataKey returns the encrypted data key.
148+
func (key *MasterKey) EncryptedDataKey() []byte {
149+
return []byte(key.EncryptedKey)
150+
}
151+
152+
// SetEncryptedDataKey sets the encrypted data key.
153+
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
154+
key.EncryptedKey = string(enc)
155+
}
156+
157+
// NeedsRotation checks if the key needs rotation.
158+
func (key *MasterKey) NeedsRotation() bool {
159+
return false
160+
}
161+
162+
// ToString returns the string representation of the key.
163+
func (key *MasterKey) ToString() string {
164+
return key.Arn
165+
}
166+
167+
// ToMap returns the map representation of the key.
168+
func (key *MasterKey) ToMap() map[string]interface{} {
169+
return map[string]interface{}{
170+
"arn": key.Arn,
171+
"created_at": key.CreationDate.UTC().Format(time.RFC3339),
172+
"enc": key.EncryptedKey,
173+
}
174+
}
175+
176+
// TypeToIdentifier returns the type identifier of the key.
177+
func (key *MasterKey) TypeToIdentifier() string {
178+
return KeyTypeIdentifier
179+
}
180+
181+
// getClient returns a new Alibaba Cloud KMS client.
182+
func (key *MasterKey) getClient() (*kmssdk.Client, error) {
183+
cred, err := credentials.NewCredential(nil)
184+
if err != nil {
185+
return nil, fmt.Errorf("acskms credential error: %v", err)
186+
}
187+
188+
config := &openapi.Config{
189+
Credential: cred,
190+
RegionId: tea.String(key.Region),
191+
}
192+
193+
if endpoint := os.Getenv("SOPS_ACSKMS_INSTANCE_ENDPOINT"); endpoint != "" {
194+
config.Endpoint = tea.String(endpoint)
195+
} else if key.Region != "" {
196+
config.Endpoint = tea.String(fmt.Sprintf("kms.%s.aliyuncs.com", key.Region))
197+
}
198+
199+
if caFile := os.Getenv("SOPS_ACSKMS_CA_FILE"); caFile != "" {
200+
caContent, err := os.ReadFile(caFile)
201+
if err == nil {
202+
config.Ca = tea.String(string(caContent))
203+
} else {
204+
log.Warnf("Failed to read CA file %s: %v", caFile, err)
205+
}
206+
}
207+
208+
client, err := kmssdk.NewClient(config)
209+
if err != nil {
210+
return nil, fmt.Errorf("acskms client error: %v", err)
211+
}
212+
return client, nil
213+
}
214+
215+
// ApplyToMasterKey applies the key parameters to the MasterKey.
216+
// Helper to reconstruct key from map.
217+
func (key *MasterKey) ApplyToMasterKey(k keys.MasterKey) {
218+
// Not strictly needed for basic interface but good to have parity
219+
}

0 commit comments

Comments
 (0)