Skip to content

chore(deployment tooling): Update Prowler API with rotating K8S access keys #6986

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 35 additions & 1 deletion docs/tutorials/prowler-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ By default, the `kubeconfig` file is located at `~/.kube/config`.
???+ note
If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication:

** Make sure your cluster allow traffic from the Prowler Cloud IP address `52.48.254.174/32` **
** Make sure your cluster allows traffic from the Prowler Cloud IP address `52.48.254.174/32` **

1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
Expand All @@ -129,6 +129,40 @@ By default, the `kubeconfig` file is located at `~/.kube/config`.

---

???+ note
Newer versions of Kubernetes have depreciated the creation of long-lived service account tokens. While there are work-arounds, these may be disabled on some managed Kubernetes clusters (or your user may not have the required Kubernetes permissions to change the settings)

In this scenario, we provide an API endpoint and local pod to dynamically update your kubeconfig when the ServiceAccount token rotates:

** As above, make sure your cluster allows traffic from the Prowler Cloud IP address `52.48.254.174/32` **

1. We use the existing prowler `ServiceAccount`, `Role` and `RoleBinding`, so as above, apply the necessary Kubernetes resources to your cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
```

2. Fill in required details for the Token Update Service via a Kubernetes Secret, there is a template `secrets.yaml` in the[`kubernetes/prowler-api-auth-updater` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes/prowler-api-auth-updater))


- api-url: The endpoint for your Prowler installation. Prowler cloud for example is https://api.prowler.com
- username: An account on your Prowler installation with permission to update your Provider configuration
- password: The credentials for the above Prowler account
- k8s-url: The public API endpoint for your Kubernetes cluster, for example on EKS this may look similar to https://ABCDEF11234567890ABCDEF.gr7.eu-west-1.eks.amazonaws.com


Then deploy the application to auto-update your prowler Kubernetes credentials.
```console
kubectl apply -f kubernetes/prowler-api-auth-updater/secret.yaml
kubectl apply -f kubernetes/prowler-api-auth-updater/deployment.yaml
```

3. You can re-test the connection from the Prowler App's Providers screen.

---


## **Step 5: Test Connection**
After adding your credentials of your cloud account, click the `Launch` button to verify that the Prowler App can successfully connect to your provider:

Expand Down
10 changes: 10 additions & 0 deletions kubernetes/prowler-api-auth-updater/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM --platform=linux/amd64 python:3.12-slim

WORKDIR /app

COPY /src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/main.py .

CMD ["python", "/app/main.py"]
57 changes: 57 additions & 0 deletions kubernetes/prowler-api-auth-updater/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: prowler-api-auth-updater
namespace: prowler-ns
spec:
replicas: 1
selector:
matchLabels:
app: prowler-api-auth-updater
template:
metadata:
labels:
app: prowler-api-auth-updater
spec:
serviceAccountName: prowler-sa
containers:
- name: watcher
image: <YOUR_IMAGE_NAME>
env:
- name: PROWLER_API_URL
valueFrom:
secretKeyRef:
name: prowler-api-credentials
key: api-url
- name: PROWLER_USERNAME
valueFrom:
secretKeyRef:
name: prowler-api-credentials
key: username
- name: PROWLER_PASSWORD
valueFrom:
secretKeyRef:
name: prowler-api-credentials
key: password
- name: K8S_EXTERNAL_HOST
valueFrom:
secretKeyRef:
name: prowler-api-credentials
key: k8s-url
volumeMounts:
- name: sa-token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: sa-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: "https://kubernetes.default.svc"
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
11 changes: 11 additions & 0 deletions kubernetes/prowler-api-auth-updater/secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: prowler-api-credentials
namespace: prowler-ns
type: Opaque
data:
api-url: <HTTPS_YOUR_PROWLER_API_URL_BASE64>
username: <YOUR_PROWLER_UI_USERNAME_BASE64>
password: <YOUR_PROWLER_UI_PASSWORD_BASE64>
k8s-url: <YOUR_PUBLIC_K8S_URL_INCL_HTTPS_BASE64>
206 changes: 206 additions & 0 deletions kubernetes/prowler-api-auth-updater/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
import os
import time
import yaml
import base64
import logging
from pathlib import Path
import requests
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
PROWLER_API_URL = os.environ["PROWLER_API_URL"]
K8S_EXTERNAL_HOST = os.environ["K8S_EXTERNAL_HOST"]
PROWLER_USERNAME = os.environ["PROWLER_USERNAME"]
PROWLER_PASSWORD = os.environ["PROWLER_PASSWORD"]
#K8S_HOST = os.environ.get("KUBERNETES_SERVICE_HOST")
#K8S_PORT = os.environ.get("KUBERNETES_SERVICE_PORT")

class TokenFileHandler(FileSystemEventHandler):
def __init__(self):
self.last_token = None
self.prowler_auth_token = self.get_prowler_auth_token()
# Initial read of the token
self.check_and_update_token()


def get_prowler_auth_token(self):

postData = {
"data": {
"type": "tokens",
"attributes": {
"email": f"{PROWLER_USERNAME}",
"password": f"{PROWLER_PASSWORD}"
}
}
}

try:
response = requests.post(
f"{PROWLER_API_URL}/api/v1/tokens",
headers={ 'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'},
json=postData
)
response.raise_for_status()
return response.json()["data"]["attributes"]["access"]
except Exception as e:
logger.error(f"Error getting Prowler auth token: {e}")
raise

def get_current_token(self):
return Path(TOKEN_PATH).read_text().strip()

def get_ca_cert(self):
return Path(CA_CERT_PATH).read_text()

def build_kubeconfig(self, token, uid):
# Read the CA cert
ca_cert_data = self.get_ca_cert()

# Build the kubeconfig structure
kubeconfig = {
"apiVersion": "v1",
"kind": "Config",
"current-context": f"{uid}",
"clusters": [{
"name": f"{uid}",
"cluster": {
"server": f"{K8S_EXTERNAL_HOST}",
"certificate-authority-data": base64.b64encode(ca_cert_data.encode()).decode()
}
}],
"contexts": [{
"name": f"{uid}",
"context": {
"cluster": f"{uid}",
"user": f"{uid}"
}
}],
"users": [{
"name": f"{uid}",
"user": {
"token": token
}
}]
}

return yaml.dump(kubeconfig)

def update_prowler_api(self, token):
try:

# Check for existing Kubernetes providers
try:
response = requests.get(
f"{PROWLER_API_URL}/api/v1/providers?filter[provider]=kubernetes",
headers={
"Authorization": f"Bearer {self.prowler_auth_token}",
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'
}
)
response.raise_for_status()
providers_data = response.json()

if len(providers_data["data"]) > 1:
logger.error("More than one Kubernetes provider found. Cannot determine the correct provider to update.")
raise Exception("More than one Kubernetes provider found.")
elif len(providers_data["data"]) == 0:
logger.error("No Kubernetes provider found.")
raise Exception("No Kubernetes provider found.")

provider = providers_data["data"][0]
secret_id = provider["relationships"]["secret"]["data"]["id"]
uid = provider["attributes"]["uid"]
except Exception as e:
logger.error(f"Error checking for existing Kubernetes providers: {e}")
raise
# Build the kubeconfig file
kubeconfig = self.build_kubeconfig(token, uid)

response = requests.patch(
f"{PROWLER_API_URL}/api/v1/providers/secrets/{secret_id}",
headers={
"Authorization": f"Bearer {self.prowler_auth_token}",
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'},
json={
"data": {
"type": "provider-secrets",
"id": secret_id,
"attributes": {
"secret": {
"kubeconfig_content": kubeconfig
},
"name": f"Kubernetes service account token - {time.strftime('%Y-%m-%d %H:%M:%S')}"
},
"relationships": {}
}
}
)
response.raise_for_status()
logger.info("Successfully updated Prowler API with new kubeconfig")
except Exception as e:
logger.error(f"Error updating Prowler API: {e}")

def check_and_update_token(self):
current_token = self.get_current_token()
if current_token != self.last_token:
logger.info("Token changed, updating Prowler API...")
self.update_prowler_api(current_token)
self.last_token = current_token

def on_modified(self, event):
if event.src_path == TOKEN_PATH:
self.check_and_update_token()

def main():
# Verify required environment variables and files
required_paths = [TOKEN_PATH, CA_CERT_PATH]
required_env = [PROWLER_API_URL, PROWLER_USERNAME, PROWLER_PASSWORD]

for path in required_paths:
if not Path(path).exists():
logger.error(f"Required file not found: {path}")
raise FileNotFoundError(f"Required file not found: {path}")

for var in required_env:
if not var:
logger.error("Missing required environment variable")
raise EnvironmentError(f"Required environment variable not set")

# Create an observer and handler
observer = Observer()
handler = TokenFileHandler()

# Schedule watching the directory containing the token
token_dir = str(Path(TOKEN_PATH).parent)
observer.schedule(handler, token_dir, recursive=True)

# Start the observer
observer.start()
logger.info(f"Started watching {TOKEN_PATH} for changes...")

try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
logger.info("Stopping token watcher...")

observer.join()

if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions kubernetes/prowler-api-auth-updater/src/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
requests>=2.31.0
watchdog>=3.0.0
PyYAML>=6.0