Skip to content

Commit

Permalink
Merge pull request CrowdStrike#186 from carlosmmatos/carlosmmatos/iss…
Browse files Browse the repository at this point in the history
…ue185

feat: add credential store to securely pass api creds
  • Loading branch information
carlosmmatos authored Jun 14, 2024
2 parents 9086335 + 32e1b4e commit 09d06a4
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 9 deletions.
132 changes: 125 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,83 @@ This project facilitates the export of the individual detections and audit event

API clients are granted one or more API scopes. Scopes allow access to specific CrowdStrike APIs and describe the actions that an API client can perform.

> [!NOTE]
> For more information on how to generate an API client, refer to the [CrowdStrike API documentation](https://falcon.crowdstrike.com/login/?unilogin=true&next=/documentation/page/a2a7fc0e/crowdstrike-oauth2-based-apis#mf8226da).
FIG requires the following API scopes at a minimum:

- **Event streams**: [Read]
- **Hosts**: [Read]

> Consult the backend guides for additional API scopes that may be required.
## Authentication

FIG requires the authentication of an API client ID and client secret, along with its associated cloud region, to establish a connection with the CrowdStrike API.

FIG supports auto-discovery of the Falcon cloud region. If you do not specify a cloud region, FIG will attempt to auto-discover the cloud region based on the API client ID and client secret provided.

> [!IMPORTANT]
> Auto-discovery is only available for [us-1, us-2, eu-1] regions.
### Direct Configuration

> [!NOTE]
> This method is not recommended for production deployments.
You can use the `config.ini` file to store your API client ID and client secret. The `config.ini` file should be located in the `config` directory. To configure authentication, add the following to the `config.ini` file:

```ini
[falcon]
cloud_region = us-1
client_id = YOUR_CLIENT_ID
client_secret = YOUR_CLIENT_SECRET
```

### Environment Variables

You can also provide your API client ID and client secret as environment variables. To do so, set the following environment variables:

```bash
export FALCON_CLOUD_REGION=us-1
export FALCON_CLIENT_ID=YOUR_CLIENT_ID
export FALCON_CLIENT_SECRET=YOUR_CLIENT_SECRET
```

### Credential Store

You can use a credential store to securely store your API client ID and client secret. FIG supports the following credential stores:

- AWS Secrets Manager (`secrets_manager`)
- AWS SSM Parameter Store (`ssm`)

> [!NOTE]
> You can use either direct configuration or environment variables to specify the credential store and its associated configurations.
To configure FIG to use a credential store, add the following to the `config.ini` file:

```ini
[falcon]
cloud_region = us-1

[credentials_store]
#store = ssm|secrets_manager
```

After selecting a credential store, you must provide the necessary configuration for the store. For example, to use AWS Secrets Manager, add the following to the `config.ini` file:

```ini
[secrets_manager]
region = YOUR_AWS_REGION
secrets_manager_secret_name = your/secret/name
secrets_manager_client_id_key = client_id_key_name
secrets_manager_client_secret_key = client_secret_key_name
```

## Configuration

Please refer to the [config.ini](./config/config.ini) file for more details on the available options along with their respective environment variables.

## Backends w/ Available Deployment Guide(s)

| Backend | Description | Deployment Guide(s) | General Guide(s) |
Expand All @@ -40,9 +110,7 @@ FIG requires the following API scopes at a minimum:

Please refer to the [FIG helm chart documentation](https://github.com/CrowdStrike/falcon-helm/tree/main/helm-charts/falcon-integration-gateway) for detailed instructions on deploying the FIG via helm chart for your respective backend(s).

### Manual Installation and Removal

#### With Docker/Podman
### With Docker/Podman

To install as a container:

Expand Down Expand Up @@ -70,28 +138,78 @@ To install as a container:
docker logs <container>
```

#### From Git Repository
### From Git Repository

1. Clone the repository
> [!NOTE]
> This method requires Python 3.7 or higher and a python package manager such as `pip` to be installed on your system.

1. Clone and navigate to the repository

```bash
git clone https://github.com/CrowdStrike/falcon-integration-gateway.git
cd falcon-integration-gateway
```

1. Install the python dependencies.

```bash
pip install -r requirements.txt
pip3 install -r requirements.txt
```

1. Modify the `./config/config.ini` file with your backend options
1. Modify the `./config/config.ini` file with your configuration options or set the associated environment variables.

1. Run the application

```bash
python3 -m fig
```

### Updating the FIG from the Git Repository

Depending on which configuration method you are using, follow the steps below to update the FIG from the Git repository.

#### config.ini

If you have made any changes to the `config.ini` file, you can update the FIG by following these steps:

1. Make a backup of the `config/config.ini` file.
1. Remove the `falcon-integration-gateway` directory.
1. Clone and navigate to the repository again.
1. Install/update the python dependencies.
1. Update the `config/config.ini` file with your configuration settings.
> Because the `config.ini` file may have new changes (ie, new sections or options), it is recommended to reapply your configuration settings from the backup to the new `config.ini` file.
1. Run the application.

An example of the process:

```bash
cp config/config.ini /tmp/config.ini
cd .. && rm -rf falcon-integration-gateway
git clone https://github.com/CrowdStrike/falcon-integration-gateway.git
cd falcon-integration-gateway
pip3 install --upgrade -r requirements.txt
# Review the new config.ini file and reapply your settings from the backup
python3 -m fig
```

This method ensures that your configuration settings are preserved while updating the FIG to the latest version.

#### Environment Variables (only)

If you are only using environment variables to configure the FIG, you can update the FIG by following these steps:

1. Pull the latest changes from the repository.
1. Install/update the python dependencies.
1. Run the application.

An example of the process:

```bash
git pull
pip3 install --upgrade -r requirements.txt
python3 -m fig
```

## [Developers Guide](./docs/developer_guide.md)

## Statement of Support
Expand Down
34 changes: 32 additions & 2 deletions config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,46 @@
# Uncomment to provide Falcon Cloud. Alternatively, use FALCON_CLOUD_REGION env variable.
#cloud_region = us-1

# Uncomment to provide OAuth Secret. Alternatively, use FALCON_CLIENT_ID env variable.
# Uncomment to provide OAuth Client ID.
# Alternatively, use FALCON_CLIENT_ID env variable or a credentials store (see [credentials_store] section).
#client_id = ABCD

# Uncomment to provide OAuth Secret. Alternatively, use FALCON_CLIENT_SECRET env variable.
# Uncomment to provide OAuth Secret.
# Alternatively, use FALCON_CLIENT_SECRET env variable or a credentials store (see [credentials_store] section).
#client_secret = ABCD

# Uncomment to provide application id. Needs to be different per each fig instance.
# Alternatively, use FALCON_APPLICATION_ID env variable.
#application_id = my-acme-gcp-1

[credentials_store]
# Uncomment to provide credentials store. Alternatively, use CREDENTIALS_STORE env variable.
# Supported values: ssm, secrets_manager
#store = ssm

[ssm]
# Uncomment to provide aws region for SSM. Alternatively, use SSM_REGION env variable.
#region = us-west-2

# Uncomment to provide SSM parameter name or path for client id. Alternatively, use SSM_CLIENT_ID env variable.
#ssm_client_id = /falcon/fig/client_id

# Uncomment to provide SSM parameter name or path for client secret. Alternatively, use SSM_CLIENT_SECRET env variable.
#ssm_client_secret = /falcon/fig/client_secret

[secrets_manager]
# Uncomment to provide aws region for Secrets Manager. Alternatively, use SECRETS_MANAGER_REGION env variable.
#region = us-west-2

# Uncomment to provide Secrets Manager secret name. Alternatively, use SECRETS_MANAGER_SECRET_NAME env variable.
#secrets_manager_secret_name = falcon/fig/credentials

# Uncomment to provide Secrets Manager client id key. Alternatively, use SECRETS_MANAGER_CLIENT_ID_KEY env variable.
#secrets_manager_client_id_key = client_id

# Uncomment to provide Secrets Manager client secret key. Alternatively, use SECRETS_MANAGER_CLIENT_SECRET_KEY env variable.
#secrets_manager_client_secret_key = client_secret

[generic]
# Generic section is applicable only when GENERIC backend is enabled in the [main] section.
# Generic backend can be used for outputting events to STDOUT
Expand Down
14 changes: 14 additions & 0 deletions config/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ application_id = fig-default-app-id
reconnect_retry_count = 36
rtr_quarantine_keyword = infected

[credentials_store]
store =

[ssm]
region =
ssm_client_id =
ssm_client_secret =

[secrets_manager]
region =
secrets_manager_secret_name =
secrets_manager_client_id_key =
secrets_manager_client_secret_key =

[generic]
# Uses client_id and client_secret from [falcon] section

Expand Down
42 changes: 42 additions & 0 deletions fig/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import configparser
from functools import cached_property
from .credstore import CredStore


class FigConfig(configparser.ConfigParser):
Expand All @@ -19,6 +20,14 @@ class FigConfig(configparser.ConfigParser):
['falcon', 'client_secret', 'FALCON_CLIENT_SECRET'],
['falcon', 'reconnect_retry_count', 'FALCON_RECONNECT_RETRY_COUNT'],
['falcon', 'application_id', 'FALCON_APPLICATION_ID'],
['credentials_store', 'store', 'CREDENTIALS_STORE'],
['ssm', 'region', 'SSM_REGION'],
['ssm', 'ssm_client_id', 'SSM_CLIENT_ID'],
['ssm', 'ssm_client_secret', 'SSM_CLIENT_SECRET'],
['secrets_manager', 'region', 'SECRETS_MANAGER_REGION'],
['secrets_manager', 'secrets_manager_secret_name', 'SECRETS_MANAGER_SECRET_NAME'],
['secrets_manager', 'secrets_manager_client_id_key', 'SECRETS_MANAGER_CLIENT_ID_KEY'],
['secrets_manager', 'secrets_manager_client_secret_key', 'SECRETS_MANAGER_CLIENT_SECRET_KEY'],
['azure', 'workspace_id', 'WORKSPACE_ID'],
['azure', 'primary_key', 'PRIMARY_KEY'],
['azure', 'arc_autodiscovery', 'ARC_AUTODISCOVERY'],
Expand All @@ -40,13 +49,46 @@ def __init__(self):
super().__init__()
self.read(['config/defaults.ini', 'config/config.ini', 'config/devel.ini'])
self._override_from_env()
self._override_from_credentials_store()

def _override_from_env(self):
for section, var, envvar in self.__class__.ENV_DEFAULTS:
value = os.getenv(envvar)
if value:
self.set(section, var, value)

def _override_from_credentials_store(self):
credentials_store = self.get('credentials_store', 'store')
if credentials_store:
self._validate_credentials_store()
region = self._get_region(credentials_store)
credstore = CredStore(self, region)
try:
client_id, client_secret = credstore.load_credentials(credentials_store)
except ValueError as e:
raise Exception("Error loading credentials from store: {}:{}".format(credentials_store, e)) from e

self.set('falcon', 'client_id', client_id)
self.set('falcon', 'client_secret', client_secret)

def _validate_credentials_store(self):
if self.get('credentials_store', 'store') not in ['ssm', 'secrets_manager']:
raise Exception('Malformed Configuration: expected credentials_store.store to be either ssm or secrets_manager')
if self.get('credentials_store', 'store') == 'ssm':
if not self.get('ssm', 'ssm_client_id') or not self.get('ssm', 'ssm_client_secret'):
raise Exception('Malformed Configuration: expected ssm_client_id and ssm_client_secret to be provided')
if self.get('credentials_store', 'store') == 'secrets_manager':
if not self.get('secrets_manager', 'secrets_manager_secret_name') or not self.get('secrets_manager', 'secrets_manager_client_id_key') or not self.get('secrets_manager', 'secrets_manager_client_secret_key'):
raise Exception('Malformed Configuration: expected secrets_manager_secret_name, secrets_manager_client_id_key and secrets_manager_client_secret_key to be provided')

def _get_region(self, credentials_store):
"""Get the region to use for credential stores."""
try:
region = self.get(credentials_store, 'region')
except configparser.NoOptionError as e:
raise Exception("No region was found for the credential store: {}".format(e)) from e
return region

def validate(self):
for section, var, envvar in self.__class__.ENV_DEFAULTS:
try:
Expand Down
74 changes: 74 additions & 0 deletions fig/config/credstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json
import boto3
from botocore.exceptions import ClientError


class CredStore:
def __init__(self, config, region):
self.config = config
self.region = region

def get_ssm_parameter(self, client, parameter_name):
"""Retrieve a parameter from AWS SSM Parameter Store."""
try:
response = client.get_parameter(Name=parameter_name, WithDecryption=True)
return response['Parameter']['Value']
except ClientError as e:
raise Exception(f"Error retrieving SSM parameter ({parameter_name}): {e}") from e

def get_secret(self, client, secret_name):
"""Retrieve a secret from AWS Secrets Manager."""
try:
response = client.get_secret_value(SecretId=secret_name)
if 'SecretString' in response:
return json.loads(response['SecretString'])
raise Exception(f"SecretString not found in the response for secret ({secret_name})")
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ResourceNotFoundException':
raise Exception(f"The requested secret ({secret_name}) was not found") from e
if error_code == 'InvalidRequestException':
raise Exception(f"The request was invalid due to: {e}") from e
if error_code == 'InvalidParameterException':
raise Exception(f"The request had invalid params: {e}") from e
raise Exception(f"Error retrieving Secrets Manager secret ({secret_name}): {e}") from e

def validate_secret_keys(self, secret, client_id_key, client_secret_key):
"""Validate the presence of required keys in the secret."""
falcon_client_id = secret.get(client_id_key)
falcon_client_secret = secret.get(client_secret_key)

if falcon_client_id is None:
raise Exception(f"The client ID key ({client_id_key}) does not exist or is None in the retrieved secret.")
if falcon_client_secret is None:
raise Exception(f"The client secret key ({client_secret_key}) does not exist or is None in the retrieved secret.")

return falcon_client_id, falcon_client_secret

def load_credentials(self, store):
"""Load credentials based on the specified store."""
if store == 'ssm':
ssm_client = boto3.client('ssm', region_name=self.region)
falcon_client_id = self.get_ssm_parameter(
ssm_client,
self.config.get('ssm', 'ssm_client_id')
)
falcon_client_secret = self.get_ssm_parameter(
ssm_client,
self.config.get('ssm', 'ssm_client_secret')
)
elif store == 'secrets_manager':
secrets_client = boto3.client('secretsmanager', region_name=self.region)
secret = self.get_secret(
secrets_client,
self.config.get('secrets_manager', 'secrets_manager_secret_name')
)
falcon_client_id, falcon_client_secret = self.validate_secret_keys(
secret,
self.config.get('secrets_manager', 'secrets_manager_client_id_key'),
self.config.get('secrets_manager', 'secrets_manager_client_secret_key')
)
else:
raise ValueError("Invalid credentials store specified.")

return falcon_client_id, falcon_client_secret

0 comments on commit 09d06a4

Please sign in to comment.