Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI arguments made order invariant #81

Merged
merged 10 commits into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Kamus ![logo](images/logo.png)
# Kamus ![logo](images/logo.png)
An open source, git-ops, zero-trust secrets encryption and decryption solution for Kubernetes applications.
Kamus enable users to easily encrypt secrets than can be decrypted only by the application running on Kubernetes.
The encryption is done using strong encryption providers (currently supported: Azure KeyVault and AES).
Expand All @@ -10,15 +10,20 @@ The simple way to run Kamus is by using the Helm chart:
helm repo add soluto https://charts.soluto.io
helm upgrade --install kamus soluto/kamus
```
Refer to the installation guide for more details.
Refer to the [installation guide](./docs/install.md) for more details.
After installing Kamus, you can start using it to encrypt secrets.
Kamus encrypt secrets for a specific application, represent by a [Kubernetes Service Account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account).
Create a service account for your application, and mount it on the pods running your application.
Now, when you know the name of the service account, and the namespace it exists in, use Kamus CLI to encrypt the secret:
Create a service account for your application, and mount it on the pods running your application.
Now, when you know the name of the service account, and the namespace it exists in, install Kamus CLI:
```
npm install -g @soluto-asurion/kamus-cli
kamus-cli encrypt super-secret kamus-example-sa default --kamus-url <Kamus URL>
```
Use Kamus CLI to encrypt the secret:
```
kamus-cli encrypt --secret super-secret --service-account kamus-example-sa --namespace default --kamus-url <Kamus URL>
```
*If you're running Kamus locally the Kamus URL will be like `http://localhost:<port>`. So you need to add `--allow-insecure-url` flag to enable http protocol.*

Pass the value returned by the CLI to your pod, and use Kamus Decrypt API to decrypt the value.
The simplest way to achieve that is by using the init container.
An alternative is to use Kamus decrypt API directly in the application code.
Expand All @@ -31,32 +36,32 @@ Kamus has 3 components:
* Decrypt API
* Key Management System (KMS)

The encrypt and decrypt APIs handle encryption and decryption requests.
The encrypt and decrypt APIs handle encryption and decryption requests.
The KMS is a wrapper for various cryptographic solutions. Currently supported:
* AES - uses one key for all secrets
* Azure KeyVault - creates one key per service account.
* Google Cloud KMS - creates one key per service account.
* Azure KeyVault - creates one key per service account.
* Google Cloud KMS - creates one key per service account.

We look forward to add support for other cloud solutions, like AWS KMS.
If you're interested in such a feature, please let us know.
We look forward to add support for other cloud solutions, like AWS KMS.
If you're interested in such a feature, please let us know.
We would like help with testing it out.
Consult the [installation guide](docs/install.md) for more details on how to deploy Kamus using the relevant KMS.

### Utilities
Kamus is shipped with 2 utilities that make it easier to use:
* Kamus CLI - a small CLI that eases the interaction with the Encrypt API. Refer to the docs for more details.
* Kamus init container - a init container that interacts with the Decrypt API. Refer to the docs for more details.

## Security
We take security seriously at Soluto.
We take security seriously at Soluto.
To learn more about the security aspects of Kamus refer to the Threat Modeling docs containing all the various threats and mitigations we discussed.
Before installing Kamus in production refer the installation guide to learn the best practices of deploying Kamus securely.
In case you find a security issue or have something you would like to discuss refer to our [security.md](security.md) policy.

## Contributing
Find a bug? Have a missing feature? Please open an issue and let us know.
Find a bug? Have a missing feature? Please open an issue and let us know.
We would like to help you using Kamus!
Please notice: Do not report security issues on GitHub.
Please notice: Do not report security issues on GitHub.
We will immediately delete such issues.

## Attribution
Expand Down
9 changes: 9 additions & 0 deletions cli/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"env": {
"browser": false,
"node": true
},
"parserOptions": {
"ecmaVersion": 2017
}
}
8 changes: 4 additions & 4 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[![npm version](https://badge.fury.io/js/%40soluto-asurion%2Fkamus-cli.svg)](https://badge.fury.io/js/%40soluto-asurion%2Fkamus-cli)
[![Known Vulnerabilities](https://snyk.io/test/github/soluto/kamus/badge.svg?targetFile=cli/package.json)](https://snyk.io/test/github/soluto/kamus) [![docker hub](https://images.microbadger.com/badges/image/soluto/kamus-cli.svg)](https://hub.docker.com/r/soluto/kamus-cli "Get your own image badge on microbadger.com")

## Kamus CLI
## Kamus CLI

This cli was created to provide an easy interface to interact with Kamus API.

It supports azure device flow authentication out of the box.
It supports azure device flow authentication out of the box.

To install, use the following NPM command:
```
Expand All @@ -24,10 +24,10 @@ kubectl run -it --rm --restart=Never kamus-cli --image=soluto/kamus-cli -- encry
#### Supported commands:

##### Encrypt
`kamus-cli encrypt <data> <serviceAccount> <namespace>`
`kamus-cli encrypt --secret <data> --service-account <serviceAccount> --namespace <namespace> --kamus-url <kamus-url> `

---
#### How to enable azure active directory authentication
#### How to enable azure active directory authentication
You need working active directory [tenant](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant) and designated [native app registration](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-register-an-app), Then just set all the `auth` prefixed options.
Once the user will run the cli with the auth options, he will get a small code and and azure URL to login into.

Expand Down
45 changes: 21 additions & 24 deletions cli/actions/encrypt.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
var bluebird = require('bluebird');
const bluebird = require('bluebird');
const opn = require('opn');
const { AuthenticationContext } = require('adal-node');
const activeDirectoryEndpoint = "https://login.microsoftonline.com/";
Expand All @@ -14,32 +14,31 @@ module.exports = async (args, options, logger) => {
_logger = logger;
if (useAuth(options)) {
const token = await acquireToken(options);
await encrypt(args, options, token);

await encrypt(options, token);
}
else {
await encrypt(args, options)
await encrypt(options);
}
}

const encrypt = async ({ data, serviceAccount, namespace }, { kamusUrl, allowInsecureUrl, certFingerprint }, token = null) => {
const encrypt = async ({ data, serviceAccount, namespace, kamusUrl, allowInsecureUrl, certFingerprint }, token = null) => {
_logger.log('Encryption started...');
_logger.log('service account:', serviceAccount);
_logger.log('namespace:', namespace);

if (!allowInsecureUrl && url.parse(kamusUrl).protocol !== "https:"){
if (!allowInsecureUrl && url.parse(kamusUrl).protocol !== 'https:') {
_logger.error("Insecure Kamus URL is not allowed");
process.exit(1);
}

try {
var response = await performEncryptRequestAsync(data, serviceAccount, namespace, kamusUrl, certFingerprint, token)
if (response.statusCode >= 300) {
const response = await performEncryptRequestAsync(data, serviceAccount, namespace, kamusUrl, certFingerprint, token);
if (response && response.statusCode >= 300) {
_logger.error(`Encrypt request failed due to unexpected error. Status code: ${response.statusCode}`);
process.exit(1);
}
_logger.info(`Successfully encrypted data to ${serviceAccount} service account in ${namespace} namespace`);
_logger.info('Encrypted data:\n' + response.body);
_logger.info(`Encrypted data:\n${response.body}`);
process.exit(0);
}
catch (err) {
Expand All @@ -49,7 +48,7 @@ const encrypt = async ({ data, serviceAccount, namespace }, { kamusUrl, allowIns
}

const acquireToken = async ({ authTenant, authApplication, authResource }) => {
const context = new AuthenticationContext(activeDirectoryEndpoint + authTenant);
const context = new AuthenticationContext(`${activeDirectoryEndpoint}${authTenant}`);
bluebird.promisifyAll(context);
refreshToken = await acquireTokenWithDeviceCode(context, authApplication, authResource);
const refreshTokenResponse =
Expand Down Expand Up @@ -78,16 +77,14 @@ const useAuth = ({ authTenant, authApplication, authResource }) => {
if (authTenant && authApplication && authResource) {
return true;
}
else {
_logger.warn('Auth options were not provided, will try to encrypt without authentication to kamus');
return false;
}
_logger.warn('Auth options were not provided, will try to encrypt without authentication to kamus');
return false;
}

//Source: http://hassansin.github.io/certificate-pinning-in-nodejs
const performEncryptRequest = (data, serviceAccount, namespace, kamusUrl, certficateFingerprint, token, cb) => {

var headers = {
const headers = {
'User-Agent': `kamus-cli-${pjson.version}`,
'Content-Type': 'application/json'
};
Expand All @@ -96,21 +93,21 @@ const performEncryptRequest = (data, serviceAccount, namespace, kamusUrl, certfi
headers['Authorization'] = `Bearer ${token}`
}

var options = {
const options = {
url: kamusUrl + '/api/v1/encrypt',
headers: headers,
// Certificate validation
strictSSL: true,
method: 'POST',
};
var req = request(options, cb);

const req = request(options, cb);

req.on('socket', socket => {
socket.on('secureConnect', () => {
var fingerprint = socket.getPeerCertificate().fingerprint;
const fingerprint = socket.getPeerCertificate().fingerprint;
// Match the fingerprint with our saved fingerprints
if(certficateFingerprint != undefined && certficateFingerprint != fingerprint){
if(certficateFingerprint !== undefined && certficateFingerprint !== fingerprint) {
// Abort request, optionally emit an error event
req.emit('error', new Error(`Server fingerprint ${fingerprint} does not match provided fingerprint ${certficateFingerprint}`));
return req.abort();
Expand All @@ -120,8 +117,8 @@ const performEncryptRequest = (data, serviceAccount, namespace, kamusUrl, certfi

req.write(JSON.stringify({
data,
"service-account": serviceAccount,
namespace
['service-account']: serviceAccount,
namespace,
}));
}

Expand Down
12 changes: 6 additions & 6 deletions cli/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
#!/usr/bin/env node

var pjson = require('./package.json');
const pjson = require('./package.json');
const prog = require('caporal');
const encrypt = require('./actions/encrypt');
const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

prog
.version(pjson.version)
.command('encrypt', 'Encrypt data')
.argument('<data>','Data to encrypt')
.argument('<service-account>', 'Deployment service account')
.argument('<namespace>', 'Deployment namespace')
.action(encrypt)
.action(encrypt)
.option('--secret <data>','Data to encrypt', prog.REQUIRED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the arguments global options, and not only for the encrypt commands - correct? Isn't it weird?

Copy link
Contributor Author

@AleF83 AleF83 Jan 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes the arguments global. I chained another command method and didn't see that arguments in the second command help.
Only arguments or options that are set before the first command method are global.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if what you're saying is true - all these:

  .option('--kamus-url <url>', 'Kamus URL', prog.REQUIRED)

Should move up before the first command...

.option('--service-account <service-account>', 'Deployment service account', prog.REQUIRED)
.option('--namespace <namespace>', 'Deployment namespace', prog.REQUIRED)
.option('--kamus-url <kamusUrl>', 'Kamus URL', prog.REQUIRED)
.option('--auth-tenant <id>', 'Azure tenant id', regexGuid)
.option('--auth-application <id>', 'Azure application id', regexGuid)
.option('--auth-resource <name>', 'Azure resource name', prog.STRING)
.option('--kamus-url <url>', 'Kamus URL', prog.REQUIRED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep it shorter - kamus-url

.option('--allow-insecure-url', 'Allow insecure (http) Kamus URL', prog.BOOL)
.option('--cert-fingerprint <certFingerprint>', 'Force server certificate to match the given fingerprint', prog.STRING);

Expand Down
4 changes: 2 additions & 2 deletions cli/is-docker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var fs = require('fs');
const fs = require('fs');

var isDocker;
let isDocker;

function hasDockerEnv() {
try {
Expand Down
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "CLI Tool to encrypt secrets for kamus",
"main": "index.js",
"scripts": {
"test": "node_modules/.bin/mocha",
"test": "node_modules/.bin/mocha ./test/*.spec.js",
"ci-publish": "ci-publish"
},
"repository": {
Expand Down Expand Up @@ -36,6 +36,7 @@
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^5.12.0",
"mocha": "^5.2.0",
"nock": "^10.0.5",
"sinon": "^7.2.2"
Expand Down
13 changes: 8 additions & 5 deletions cli/test/encrypt.js → cli/test/encrypt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const expect = require('chai').expect;
const nock = require('nock');
const sinon = require('sinon');

const encrypt = require('../actions/encrypt.js');
const encrypt = require('../actions/encrypt');

const logger =
const logger =
{
info: sinon.spy(),
error: console.error,
Expand All @@ -17,16 +17,19 @@ const data = 'super-secret';
const serviceAccount = 'dummy';
const namespace = 'team-a';

let kamusApiScope;

describe('Encrypt', () => {
beforeEach(() => {
sinon.stub(process, 'exit');
nock(kamusUrl)
.post('/api/v1/encrypt', { data, "service-account": serviceAccount, namespace})
kamusApiScope = nock(kamusUrl)
.post('/api/v1/encrypt', { data, ['service-account']: serviceAccount, namespace})
.reply(200, '123ABC');
});

it('Should return encrypted data', async () => {
await encrypt({data, serviceAccount, namespace}, {kamusUrl}, logger);
await encrypt(null, {data, serviceAccount, namespace, kamusUrl}, logger);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why null is added here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caporal calls the action with 3 parameters: args, options, logger.
I removed the first parameter by mistake because we don't actually use it. But the order matters.

expect(kamusApiScope.isDone()).to.be.true;
expect(process.exit.called).to.be.true;
expect(process.exit.calledWith(0)).to.be.true;
expect(logger.info.lastCall.lastArg).to.equal('Encrypted data:\n123ABC')
Expand Down
Loading