-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5d63d2e
commit 0bbcf53
Showing
21 changed files
with
44,427 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Arky Asmal | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,183 @@ | ||
# Github-Vault-Action | ||
# Hashicorp Vault Secrets Github Action | ||
|
||
## Introduction | ||
|
||
Currently, [Hashicorp Vault Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) has a direct one-click intergation that links a Github Repo to an app of their choosing. | ||
|
||
However, as highlighted by their [documentation](https://developer.hashicorp.com/hcp/docs/vault-secrets/integrations/github-actions), there are severe limitations. For example: | ||
|
||
1. You can only sync secrets from a single Hashicorp Cloud Platform project | ||
2. You can only sync a single organization with a repository. | ||
3. This integration requires the Hashicorp Vault Secrets App to be installed and configured in your repository | ||
- This is not possible if the repository lives in an organization and the user is not a Github organization owner/admin. | ||
|
||
This action provides a solution for the aforementioned problems, by using a service principal on your HashiCorp Cloud Platform account, to programmatically access Hashicorp Vault secrets in a Github action runner, and pass them into your workflows. | ||
|
||
## Configuring a Service Principal | ||
|
||
### Requirements: | ||
|
||
- You must be using a HCP Vault Secrets App | ||
- You must be a HashiCorp Cloud Platform organization Admin or Owner | ||
|
||
### Steps: | ||
|
||
1. Go [here](https://portal.cloud.hashicorp.com/sign-in) and login | ||
2. Go to your organization | ||
3. Go to **Access Control IAM**. Go to **Service Principals** and create a service principal account | ||
|
||
##### Service Princpal Page Example | ||
|
||
 | ||
|
||
## Action Usage | ||
|
||
### Quickstart | ||
|
||
```yaml | ||
name: Hashicorp Vault Secrets | ||
uses: aasmal97/[email protected] | ||
with: | ||
CLIENT_ID: ${{ secrets.HASHICORP_CLIENT_ID }} | ||
CLIENT_SECRET: ${{ secrets.HASHICORP_CLIENT_SECRET }} | ||
ORGANIZATION_ID: "xxx" | ||
PROJECT_ID: "xxx" | ||
APP_NAME: "xxx" | ||
SECRET_NAMES: '["EXAMPLE_NAME"]' | ||
``` | ||
### Inputs: | ||
- ##### CLIENT_ID: `string` (required) | ||
- This is the Organization Service Principal's generated CLIENT_ID acquired from your Hashicorp Portal. | ||
- ##### CLIENT_SECRET: `string` (required) | ||
- This is the Organization Service Principal's generated CLIENT_SECRET acquired from your Hashicorp Portal. | ||
- ##### ORGANIZATION_ID: `string` (required) | ||
- This is the Organization ID that the Service Principal was created on. To access this, go to your organization settings | ||
- ##### PROJECT_ID: `string` (required) | ||
- This is the project ID that holds the apps where the secrets are stored. To access this, go to your project's settings | ||
- ##### APP_NAME: `string` (required) | ||
|
||
- This is the app name, that holds the secrets | ||
|
||
- ##### SECRET_NAMES: `string` (optional) | ||
|
||
- This is **JSON Stringified List** of the secret names you want to extract. | ||
- To ensure your list of variables have the correct syntax, pass your array/list through a JSON.stringifier and pass the resulting string in here. | ||
- Note: We use `JSON.parse` to parse this string into a list since GitHub Actions does not currently support a list input | ||
|
||
- ##### GENERATE_ENV: `string` (optional) | ||
|
||
- The name of the `.env` file that you wish to generate. If your name contains a _`.`_, your provided name will become the file name of the `.env` file. If not, it will become the `{name} + .env` | ||
|
||
For example: | ||
|
||
- `mysecrets.env.local` as the `GENERATE_ENV` value, becomes `mysecrets.env.local`. | ||
- `mysecrets` as the `GENERATE_ENV` value, becomes `mysecrets.env` | ||
|
||
- ##### ALL_SECRETS: `boolean` (optional) | ||
- If you want to grab all the secrets on the hashicorp vault secrets app, set this to `true`. By default, this is `false`. If this is set, you do not need to set `SECRET_NAMES` | ||
|
||
### Using Action Output | ||
|
||
#### In a Github Action job | ||
|
||
To use this action's output in subsequent workflow steps, ensure your `id` from the running action step, is the key to the subsquent step. | ||
|
||
##### Example: | ||
|
||
```yaml | ||
steps: | ||
- name: Hashicorp Vault Secrets | ||
id: hashicorp-vault-secrets | ||
uses: aasmal97/[email protected] | ||
with: | ||
CLIENT_ID: ${{ secrets.HASHICORP_CLIENT_ID }} | ||
CLIENT_SECRET: ${{ secrets.HASHICORP_CLIENT_SECRET }} | ||
ORGANIZATION_ID: "xxx" | ||
PROJECT_ID: "xxx" | ||
APP_NAME: "xxx" | ||
SECRET_NAMES: '["EXAMPLE_NAME"]' | ||
- name: Example Step | ||
run: echo "The output value is ${{ steps.hashicorp-vault-secrets.outputs.EXAMPLE_NAME }}" | ||
``` | ||
|
||
#### Using a generated .env file | ||
|
||
To use this, you must use the `GENERATE_ENV` input. | ||
|
||
```yaml | ||
steps: | ||
- name: Hashicorp Vault Secrets | ||
uses: aasmal97/[email protected] | ||
with: | ||
CLIENT_ID: ${{ secrets.HASHICORP_CLIENT_ID }} | ||
CLIENT_SECRET: ${{ secrets.HASHICORP_CLIENT_SECRET }} | ||
ORGANIZATION_ID: "xxx" | ||
PROJECT_ID: "xxx" | ||
APP_NAME: "xxx" | ||
SECRET_NAMES: '["EXAMPLE_NAME"]' | ||
GENERATE_ENV: "example.env" | ||
- name: Check if example.env exists | ||
shell: bash | ||
run: | | ||
if test -f /example.env; then | ||
echo "File exists." | ||
fi | ||
``` | ||
|
||
#### Load all secrets in Vault Secrets App | ||
|
||
```yaml | ||
steps: | ||
- name: Hashicorp Vault Secrets | ||
id: hashicorp-vault-secrets | ||
uses: aasmal97/[email protected] | ||
with: | ||
CLIENT_ID: ${{ secrets.HASHICORP_CLIENT_ID }} | ||
CLIENT_SECRET: ${{ secrets.HASHICORP_CLIENT_SECRET }} | ||
ORGANIZATION_ID: "xxx" | ||
PROJECT_ID: "xxx" | ||
APP_NAME: "xxx" | ||
ALL_SECRETS: true | ||
- name: Example Step | ||
run: echo "The output value is ${{ steps.hashicorp-vault-secrets.outputs.EXAMPLE_NAME }}" | ||
``` | ||
|
||
## Limitations | ||
|
||
- The service principal account must be configured at the **Organization Level**. This limitation is imposed by Hashicorp themselves, and until this changes, there can't be support for more granular access (i.e service principal for only a project). | ||
- The `SECRET_NAMES` must be a string since list inputs are not supported by Github Actions. In the future, this may be changed, when Github supports list inputs natively. | ||
- This action can only run in **ubuntu** environments. It is not supported in darwin or mac. This is due primarily to ubuntu being the most common environment for Github action runners, but it is also due to my lack of hardware and time. However, in the future, support can be added if it is seen as a good or necessary feature. | ||
|
||
## Contributing | ||
|
||
Anyone is welcome to contribute, simply open an issue or pull request. When opening an issue, ensure you can reproduce the issue, and list the steps you took to reproduce it. | ||
|
||
### Development Environment | ||
|
||
To run the development environment, ensure the following are configured properly, and your are running the appropiate commands. | ||
|
||
#### Requirements | ||
|
||
- [Docker](https://docs.docker.com/engine/install/) installed on your machine. It will provide the virtual environment needed to run a Github Action | ||
- [nektos/act](https://github.com/nektos/act) installed. This is the software that uses Docker to create a container, that resembles a Github Action Environment for testing | ||
- Have a package manager installed (i.e, npm, yarn, etc) | ||
- Create a Hashicorp Cloud Platform Account | ||
1. Go [here](https://portal.cloud.hashicorp.com/sign-in) and create an account | ||
2. Create a dummy organization | ||
3. Go to **Access Control IAM**, then go to **Service Principals** and create a dummy service principal account | ||
- **Save** the **_Client ID_** and **_Client Secret_** values in a `my.secrets` file in the following path `test/workflows/my.secrets`. `nektos/act` will use this to run the virtual github action. | ||
- Note: The `my.secrets` file follows the same form/syntax as a regular `.env` file. | ||
4. Create a dummy project in your organization | ||
5. Click on newly created dummy project, and go to **Vault Secrets** | ||
6. Go to **Applications** and create a dummy application | ||
7. Fill in the dummy application with dummy secrets | ||
|
||
#### Running Dev Environment | ||
|
||
1. Run `npm i` | ||
2. Run `npm run dev` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
name: HashiCorp Vault Secrets | ||
author: Arky Asmal | ||
description: Access secrets stored on HashiCorp Vault Secrets, in the HashiCorp Cloud Platform. | ||
branding: | ||
icon: "briefcase" | ||
color: "black" | ||
inputs: | ||
CLIENT_ID: | ||
default: "" | ||
description: Client ID of the HashiCorp Cloud Service Principal/User | ||
required: true | ||
CLIENT_SECRET: | ||
default: "" | ||
description: Client Secret of the HashiCorp Cloud Service Principal/User | ||
required: true | ||
ORGANIZATION_ID: | ||
default: "" | ||
description: ID of the HashiCorp Cloud Organization | ||
required: true | ||
PROJECT_ID: | ||
default: "" | ||
description: ID of project deployed on HashiCorp Cloud Platform | ||
required: true | ||
APP_NAME: | ||
default: "" | ||
description: Name of app deployed on HashiCorp Cloud Platform, using HashiCorp Vault Secrets | ||
required: true | ||
SECRET_NAMES: | ||
default: "[]" | ||
description: a list of secret names, in an app using HashiCorp Vault Secrets | ||
required: false | ||
GENERATE_ENV: | ||
default: "" | ||
description: The name of the .env file to be generated. If not set, no .env is generated. By default this is not set. | ||
required: false | ||
ALL_SECRETS: | ||
default: "false" | ||
description: If set to true, all secrets are retrieved, otherwise only the secrets specified in SECRET_NAMES are retrieved. | ||
required: false | ||
runs: | ||
using: "node20" | ||
main: "dist/action/index.js" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import * as core from "@actions/core"; | ||
import { generateEnvFile } from "../utils/generateEnv"; | ||
import { extractSecrets } from "./utils/getSecretNames"; | ||
import { installHashiCorp } from "./utils/installHashiCorp"; | ||
import { z } from "zod"; | ||
export const ActionSchema = z.object({ | ||
clientId: z.string({ | ||
required_error: "CLIENT_ID is required", | ||
invalid_type_error: "CLIENT_ID must be a string", | ||
}), | ||
clientSecret: z.string({ | ||
required_error: "CLIENT_SECRET is required", | ||
invalid_type_error: "CLIENT_SECRET must be a string", | ||
}), | ||
organizationName: z.string({ | ||
required_error: "ORGANIZATION_ID is required", | ||
invalid_type_error: "ORGANIZATION_ID must be a string", | ||
}), | ||
projectName: z.string({ | ||
required_error: "PROJECT_ID is required", | ||
invalid_type_error: "PROJECT_ID must be a string", | ||
}), | ||
appName: z.string({ | ||
required_error: "APP_NAME is required", | ||
invalid_type_error: "APP_NAME must be a string", | ||
}), | ||
secretsNames: z | ||
.string({ | ||
invalid_type_error: "SECRET_NAMES must be a JSON Stringified Array", | ||
}) | ||
.or(z.array(z.string())) | ||
.optional() | ||
.transform((val) => { | ||
if (typeof val === "string") return JSON.parse(val) as string[]; | ||
return val; | ||
}), | ||
generateEnv: z | ||
.string({ invalid_type_error: "GENERATE_ENV must be a string" }) | ||
.optional() | ||
.transform((val) => { | ||
if (!val) return; | ||
const hasExtension = val.split(".").length >= 2; | ||
if (hasExtension) return val; | ||
else return `${val}.env`; | ||
}), | ||
allSecrets: z | ||
.boolean() | ||
.or(z.string()) | ||
.optional() | ||
.default(false) | ||
.transform((val) => { | ||
if (typeof val === "boolean") return val; | ||
if (typeof val === "string") return val === "true"; | ||
return false; | ||
}), | ||
}); | ||
export type ActionSchemaType = z.infer<typeof ActionSchema>; | ||
export const getInputs = () => { | ||
core.info("Getting Inputs"); | ||
const clientId = core.getInput("CLIENT_ID"); | ||
const clientSecret = core.getInput("CLIENT_SECRET"); | ||
const organizationName = core.getInput("ORGANIZATION_ID"); | ||
const projectName = core.getInput("PROJECT_ID"); | ||
const appName = core.getInput("APP_NAME"); | ||
const secretsNames = core.getInput("SECRET_NAMES"); | ||
const generateEnv = core.getInput("GENERATE_ENV"); | ||
const allSecrets = core.getInput("ALL_SECRETS"); | ||
const data = { | ||
clientId, | ||
clientSecret, | ||
projectName, | ||
appName, | ||
secretsNames, | ||
generateEnv, | ||
organizationName, | ||
allSecrets, | ||
}; | ||
const paramsValidationResult = ActionSchema.safeParse(data); | ||
if (!paramsValidationResult.success) { | ||
core.setFailed(paramsValidationResult.error.message); | ||
return new Error(paramsValidationResult.error.message); | ||
} | ||
core.info("Inputs Parsed"); | ||
return paramsValidationResult.data; | ||
}; | ||
|
||
export const main = async () => { | ||
const inputs = getInputs(); | ||
if (inputs instanceof Error) return; | ||
const { | ||
clientId, | ||
clientSecret, | ||
projectName, | ||
appName, | ||
secretsNames, | ||
generateEnv, | ||
organizationName, | ||
allSecrets, | ||
} = inputs; | ||
installHashiCorp({ | ||
clientId, | ||
clientSecret, | ||
}); | ||
const [content, output] = await extractSecrets({ | ||
secretNames: secretsNames, | ||
allSecrets, | ||
config: { | ||
appName, | ||
projectName, | ||
organizationName, | ||
}, | ||
auth: { | ||
clientId, | ||
clientSecret, | ||
}, | ||
}); | ||
core.info("Finished secrets generation"); | ||
Object.keys(output).forEach((key) => { | ||
//mask the value | ||
core.setSecret(output[key]); | ||
//set output | ||
core.setOutput(key, output[key]); | ||
}); | ||
if (generateEnv) generateEnvFile(generateEnv, content); | ||
}; | ||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export type HashiCorpAuthOptions = { | ||
clientId: string; | ||
clientSecret: string; | ||
}; | ||
export type HashiCorpConfigOptions = { | ||
projectName: string; | ||
appName: string; | ||
organizationName: string; | ||
}; |
Oops, something went wrong.