@highstate/sops 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @highstate/sops
2
+
3
+ A Highstate package that implements SOPS (Secrets OPerationS) encryption for secrets management, using SSH host keys from servers as age recipients.
4
+
5
+ ## Overview
6
+
7
+ This package provides a `secrets` unit that encrypts sensitive data using SOPS with age encryption. It automatically derives age recipients from the SSH host keys of the provided servers, allowing for secure secrets distribution across your infrastructure.
8
+
9
+ ## Features
10
+
11
+ - **SSH Host Key Integration**: Automatically uses SSH host keys from servers as encryption keys
12
+ - **Age Encryption**: Leverages age encryption (modern alternative to PGP) via SOPS
13
+ - **SOPS Compatible**: Generates files compatible with standard SOPS tools
14
+ - **Embedded File Output**: Produces encrypted content as an embedded file entity
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import { sops } from "@highstate/library"
20
+
21
+ // Define your servers with SSH host keys
22
+ const servers = [
23
+ // Your server instances with SSH configurations
24
+ ]
25
+
26
+ // Encrypt secrets using SOPS
27
+ const encryptedSecrets = sops.secrets({
28
+ name: "my-secrets",
29
+ secrets: {
30
+ data: {
31
+ databasePassword: "super-secret-password",
32
+ apiKey: "my-api-key-12345",
33
+ certificateData: "-----BEGIN CERTIFICATE-----\n...",
34
+ },
35
+ },
36
+ inputs: {
37
+ servers: servers,
38
+ },
39
+ })
40
+
41
+ // Access the encrypted file
42
+ const secretsFile = encryptedSecrets.file
43
+ ```
44
+
45
+ ## How It Works
46
+
47
+ 1. **SSH Key Extraction**: The unit extracts SSH host keys from the provided servers
48
+ 2. **Age Conversion**: SSH keys are converted to age recipients format (currently simulated)
49
+ 3. **SOPS Encryption**: Secrets are encrypted using SOPS with the derived age recipients
50
+ 4. **File Generation**: An encrypted SOPS file is generated and embedded
51
+
52
+ ## Current Implementation Status
53
+
54
+ This is a demonstration implementation that:
55
+
56
+ - ✅ Correctly extracts SSH host keys from servers
57
+ - ✅ Generates SOPS-compatible encrypted file structure
58
+ - ⚠️ Uses mock encryption (not real SOPS encryption)
59
+ - ⚠️ Simulates SSH-to-age key conversion
60
+
61
+ ## Production Deployment
62
+
63
+ For production use, this implementation would need:
64
+
65
+ 1. **Real SSH-to-Age Conversion**: Integration with tools like [ssh-to-age](https://github.com/Mic92/ssh-to-age)
66
+ 2. **Actual SOPS Binary**: Installation and usage of the real SOPS binary
67
+ 3. **Proper Key Management**: Secure handling of converted age keys
68
+
69
+ Example production command that would be used:
70
+
71
+ ```bash
72
+ sops encrypt --age "age1..." secrets.yaml
73
+ ```
74
+
75
+ ## Dependencies
76
+
77
+ - `@highstate/pulumi` - For Pulumi integration
78
+ - `@highstate/library` - For unit definitions
79
+ - `@highstate/common` - For Command execution
80
+ - `remeda` - For utility functions
81
+
82
+ ## File Structure
83
+
84
+ ```
85
+ packages/sops/
86
+ ├── package.json
87
+ ├── src/
88
+ │ ├── index.ts # Package exports
89
+ │ └── secrets/
90
+ │ └── index.ts # Main secrets unit implementation
91
+ └── README.md # This file
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ The unit expects servers with SSH configurations:
97
+
98
+ ```typescript
99
+ {
100
+ ssh: {
101
+ hostKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
102
+ // ... other SSH configuration
103
+ }
104
+ }
105
+ ```
106
+
107
+ Supported SSH key types:
108
+
109
+ - `ssh-ed25519` (recommended)
110
+ - `ssh-rsa` (legacy support)
111
+
112
+ ## Security Considerations
113
+
114
+ - SSH host keys should be verified and trusted
115
+ - In production, proper SOPS encryption ensures secrets are protected
116
+ - Age encryption provides forward secrecy and quantum resistance
117
+ - Always verify the integrity of encrypted files before deployment
118
+
119
+ ## Related Tools
120
+
121
+ - [SOPS](https://getsops.io/) - The underlying encryption tool
122
+ - [age](https://age-encryption.org/) - Modern encryption tool
123
+ - [ssh-to-age](https://github.com/Mic92/ssh-to-age) - SSH to age key converter
@@ -0,0 +1,5 @@
1
+ {
2
+ "sourceHashes": {
3
+ "./dist/secrets/index.js": 325534342
4
+ }
5
+ }
@@ -0,0 +1,48 @@
1
+ import { sops } from '@highstate/library';
2
+ import { forUnit, toPromise } from '@highstate/pulumi';
3
+ import { Command, MaterializedFile } from '@highstate/common';
4
+ import { isNonNullish } from 'remeda';
5
+
6
+ // src/secrets/index.ts
7
+ var { name, inputs, secrets, outputs } = forUnit(sops.secrets);
8
+ var servers = await toPromise(inputs.servers ?? []);
9
+ var secretsData = await toPromise(secrets.data);
10
+ if (servers.length === 0) {
11
+ throw new Error("At least one server must be provided");
12
+ }
13
+ var serversWithSsh = servers.filter((server) => server.ssh?.hostKey);
14
+ if (serversWithSsh.length === 0) {
15
+ throw new Error("No servers with SSH host keys found");
16
+ }
17
+ var ageKeys = await toPromise(
18
+ serversWithSsh.map((server) => server.ssh?.hostKey).filter(isNonNullish).map((hostKey, index) => {
19
+ return new Command(`ssh-to-age-${index}`, {
20
+ host: "local",
21
+ create: `echo "${hostKey}" | ssh-to-age`
22
+ }).stdout;
23
+ })
24
+ );
25
+ var dataFile = await MaterializedFile.create(
26
+ "data.json",
27
+ JSON.stringify(secretsData, null, 2),
28
+ 384
29
+ );
30
+ var encryptCommand = new Command("sops-encrypt", {
31
+ host: "local",
32
+ create: `sops encrypt --age ${ageKeys.join(",")} ${dataFile.path}`
33
+ });
34
+ var secrets_default = outputs({
35
+ file: {
36
+ meta: {
37
+ name: `${name}.json`
38
+ },
39
+ content: {
40
+ type: "embedded",
41
+ value: encryptCommand.stdout
42
+ }
43
+ }
44
+ });
45
+
46
+ export { secrets_default as default };
47
+ //# sourceMappingURL=index.js.map
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/secrets/index.ts"],"names":[],"mappings":";;;;;;AAKA,IAAM,EAAE,MAAM,MAAQ,EAAA,OAAA,EAAS,SAAY,GAAA,OAAA,CAAQ,KAAK,OAAO,CAAA;AAE/D,IAAM,UAAU,MAAM,SAAA,CAAU,MAAO,CAAA,OAAA,IAAW,EAAE,CAAA;AACpD,IAAM,WAAc,GAAA,MAAM,SAAU,CAAA,OAAA,CAAQ,IAAI,CAAA;AAEhD,IAAI,OAAA,CAAQ,WAAW,CAAG,EAAA;AACxB,EAAM,MAAA,IAAI,MAAM,sCAAsC,CAAA;AACxD;AAEA,IAAM,iBAAiB,OAAQ,CAAA,MAAA,CAAO,CAAU,MAAA,KAAA,MAAA,CAAO,KAAK,OAAO,CAAA;AACnE,IAAI,cAAA,CAAe,WAAW,CAAG,EAAA;AAC/B,EAAM,MAAA,IAAI,MAAM,qCAAqC,CAAA;AACvD;AAGA,IAAM,UAAU,MAAM,SAAA;AAAA,EACpB,cACG,CAAA,GAAA,CAAI,CAAU,MAAA,KAAA,MAAA,CAAO,GAAK,EAAA,OAAO,CACjC,CAAA,MAAA,CAAO,YAAY,CAAA,CACnB,GAAI,CAAA,CAAC,SAAS,KAAU,KAAA;AACvB,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAc,WAAA,EAAA,KAAK,CAAI,CAAA,EAAA;AAAA,MACxC,IAAM,EAAA,OAAA;AAAA,MACN,MAAA,EAAQ,SAAS,OAAO,CAAA,cAAA;AAAA,KACzB,CAAE,CAAA,MAAA;AAAA,GACJ;AACL,CAAA;AAEA,IAAM,QAAA,GAAW,MAAM,gBAAiB,CAAA,MAAA;AAAA,EACtC,WAAA;AAAA,EACA,IAAK,CAAA,SAAA,CAAU,WAAa,EAAA,IAAA,EAAM,CAAC,CAAA;AAAA,EACnC;AACF,CAAA;AAGA,IAAM,cAAA,GAAiB,IAAI,OAAA,CAAQ,cAAgB,EAAA;AAAA,EACjD,IAAM,EAAA,OAAA;AAAA,EACN,MAAA,EAAQ,sBAAsB,OAAQ,CAAA,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,EAAI,SAAS,IAAI,CAAA;AAClE,CAAC,CAAA;AAED,IAAO,kBAAQ,OAAQ,CAAA;AAAA,EACrB,IAAM,EAAA;AAAA,IACJ,IAAM,EAAA;AAAA,MACJ,IAAA,EAAM,GAAG,IAAI,CAAA,KAAA;AAAA,KACf;AAAA,IACA,OAAS,EAAA;AAAA,MACP,IAAM,EAAA,UAAA;AAAA,MACN,OAAO,cAAe,CAAA;AAAA;AACxB;AAEJ,CAAC","file":"index.js","sourcesContent":["import { sops } from \"@highstate/library\"\nimport { forUnit, toPromise } from \"@highstate/pulumi\"\nimport { Command, MaterializedFile } from \"@highstate/common\"\nimport { isNonNullish } from \"remeda\"\n\nconst { name, inputs, secrets, outputs } = forUnit(sops.secrets)\n\nconst servers = await toPromise(inputs.servers ?? [])\nconst secretsData = await toPromise(secrets.data)\n\nif (servers.length === 0) {\n throw new Error(\"At least one server must be provided\")\n}\n\nconst serversWithSsh = servers.filter(server => server.ssh?.hostKey)\nif (serversWithSsh.length === 0) {\n throw new Error(\"No servers with SSH host keys found\")\n}\n\n// convert each SSH key to age key\nconst ageKeys = await toPromise(\n serversWithSsh\n .map(server => server.ssh?.hostKey)\n .filter(isNonNullish)\n .map((hostKey, index) => {\n return new Command(`ssh-to-age-${index}`, {\n host: \"local\",\n create: `echo \"${hostKey}\" | ssh-to-age`,\n }).stdout\n }),\n)\n\nconst dataFile = await MaterializedFile.create(\n \"data.json\",\n JSON.stringify(secretsData, null, 2),\n 0o600,\n)\n\n// encrypt secrets using sops\nconst encryptCommand = new Command(\"sops-encrypt\", {\n host: \"local\",\n create: `sops encrypt --age ${ageKeys.join(\",\")} ${dataFile.path}`,\n})\n\nexport default outputs({\n file: {\n meta: {\n name: `${name}.json`,\n },\n content: {\n type: \"embedded\",\n value: encryptCommand.stdout,\n },\n },\n})\n"]}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@highstate/sops",
3
+ "version": "0.9.16",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "src"
8
+ ],
9
+ "exports": {
10
+ "./secrets": "./dist/secrets/index.js"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "highstate build"
17
+ },
18
+ "dependencies": {
19
+ "@highstate/common": "^0.9.16",
20
+ "@highstate/contract": "^0.9.16",
21
+ "@highstate/library": "^0.9.16",
22
+ "@highstate/pulumi": "^0.9.16",
23
+ "remeda": "^2.21.0"
24
+ },
25
+ "devDependencies": {
26
+ "@highstate/cli": "^0.9.16"
27
+ },
28
+ "gitHead": "458d6f1f9f6d4aec0ba75a2b2c4c01408cb9c8df"
29
+ }
@@ -0,0 +1,55 @@
1
+ import { sops } from "@highstate/library"
2
+ import { forUnit, toPromise } from "@highstate/pulumi"
3
+ import { Command, MaterializedFile } from "@highstate/common"
4
+ import { isNonNullish } from "remeda"
5
+
6
+ const { name, inputs, secrets, outputs } = forUnit(sops.secrets)
7
+
8
+ const servers = await toPromise(inputs.servers ?? [])
9
+ const secretsData = await toPromise(secrets.data)
10
+
11
+ if (servers.length === 0) {
12
+ throw new Error("At least one server must be provided")
13
+ }
14
+
15
+ const serversWithSsh = servers.filter(server => server.ssh?.hostKey)
16
+ if (serversWithSsh.length === 0) {
17
+ throw new Error("No servers with SSH host keys found")
18
+ }
19
+
20
+ // convert each SSH key to age key
21
+ const ageKeys = await toPromise(
22
+ serversWithSsh
23
+ .map(server => server.ssh?.hostKey)
24
+ .filter(isNonNullish)
25
+ .map((hostKey, index) => {
26
+ return new Command(`ssh-to-age-${index}`, {
27
+ host: "local",
28
+ create: `echo "${hostKey}" | ssh-to-age`,
29
+ }).stdout
30
+ }),
31
+ )
32
+
33
+ const dataFile = await MaterializedFile.create(
34
+ "data.json",
35
+ JSON.stringify(secretsData, null, 2),
36
+ 0o600,
37
+ )
38
+
39
+ // encrypt secrets using sops
40
+ const encryptCommand = new Command("sops-encrypt", {
41
+ host: "local",
42
+ create: `sops encrypt --age ${ageKeys.join(",")} ${dataFile.path}`,
43
+ })
44
+
45
+ export default outputs({
46
+ file: {
47
+ meta: {
48
+ name: `${name}.json`,
49
+ },
50
+ content: {
51
+ type: "embedded",
52
+ value: encryptCommand.stdout,
53
+ },
54
+ },
55
+ })