Compare commits

...
26 Commits
Author SHA1 Message Date
Adrien Bouvais 4de69e5f5f Removed tenant in path 2026-09-24 10:45:15 +02:00
Adrien Bouvais 3939d3e541 Changed some comments for presentation 2026-09-16 20:12:36 +02:00
Adrien Bouvais cb08ae231e remove new tenant 2026-09-16 20:12:18 +02:00
Adrien Bouvais 5758410bef New tenant 2026-09-16 20:09:56 +02:00
adrien b7be87b668 Update README.md 2026-09-15 23:16:08 +00:00
Adrien Bouvais 70a67c7009 Fix minio image url 2026-09-16 01:15:11 +02:00
Adrien Bouvais df08869b8b Delete tenant-c 2026-08-17 13:11:26 +02:00
Adrien Bouvais ea28a0dccd Updated README to reflect minio admin password changes 2026-08-17 12:41:22 +02:00
Adrien Bouvais f3b3af1feb Fix path 2026-08-17 12:39:16 +02:00
Adrien Bouvais 77b588c450 Added demo file to minio setup ref bucket 2026-08-17 12:37:33 +02:00
Adrien Bouvais 45d9ffdc77 Synthax again 2026-08-17 12:35:32 +02:00
Adrien Bouvais adfa295255 Synthax 2026-08-17 12:33:45 +02:00
Adrien Bouvais 2710c63b0e Missing key in secret 2026-08-17 12:32:31 +02:00
Adrien Bouvais 4507b46853 Synthax 2026-08-17 12:26:25 +02:00
Adrien Bouvais efd8dab852 Fix names 2026-08-17 12:20:29 +02:00
adrien 28cd5c00aa Merge pull request 'Fixs' (#5) from test into main
Reviewed-on: #5
2026-08-17 10:08:17 +00:00
Adrien Bouvais 9ca95ca252 Fix minio setup
Build and Push Docker Images / build-and-push (pull_request) Successful in 27s
Validate Kubernetes & YAML / validate-all (pull_request) Successful in 36s
2026-08-17 12:06:22 +02:00
Adrien Bouvais 9c7422f2f2 Added openssl to minio setup runner 2026-08-17 11:55:11 +02:00
Adrien Bouvais 09f48bf0e3 Added tenant-c 2026-08-17 11:48:45 +02:00
Adrien Bouvais 6a5bd13a13 Added credentials to docker image pull 2026-08-17 11:42:24 +02:00
adrien f0821d2cfe Merge pull request 'Changed minio runner to use custom image' (#4) from test into main
Reviewed-on: #4
2026-08-17 09:39:51 +00:00
Adrien Bouvais b470314318 Changed minio runner to use custom image
Build and Push Docker Images / build-and-push (pull_request) Successful in 24s
Validate Kubernetes & YAML / validate-all (pull_request) Successful in 37s
2026-08-17 11:39:18 +02:00
adrien f459b11e8b Merge pull request 'Stop propagating minio admin credentials to each tenants' (#3) from test into main
Reviewed-on: #3
2026-08-17 09:29:37 +00:00
Adrien Bouvais 174c5faf6b Stop propagating minio admin credentials to each tenants
Build and Push Docker Images / build-and-push (pull_request) Successful in 22s
Validate Kubernetes & YAML / validate-all (pull_request) Successful in 36s
2026-08-17 11:29:00 +02:00
adrien b9eeab3e57 Merge pull request 'Synthax on README' (#2) from test into main
Reviewed-on: #2
2026-08-17 09:06:15 +00:00
Adrien Bouvais 32cffaad39 Synthax on README
Build and Push Docker Images / build-and-push (pull_request) Successful in 22s
Validate Kubernetes & YAML / validate-all (pull_request) Successful in 40s
2026-08-17 11:05:01 +02:00
9 changed files with 144 additions and 96 deletions
+28 -22
View File
@@ -16,7 +16,7 @@ the tenant resources, and a shared **MinIO** instance for S3-compatible storage.
```mermaid
graph TD
subgraph Git Repository
A[GitOps: gitops/tenants/] -->|Contains| B(tenant-a/config.yaml)
A[GitOps: tenants/] -->|Contains| B(tenant-a/config.yaml)
A -->|Contains| C(tenant-b/config.yaml)
end
@@ -26,28 +26,34 @@ graph TD
E -->|Deploys via Helm| F[Namespace: tenant-a]
E -->|Deploys via Helm| G[Namespace: tenant-b]
subgraph argo [Namespace: argocd]
D(Argo CD)
E(ApplicationSet)
end
subgraph F [Namespace: tenant-a]
F1(Jupyter Pod)
F2(ServiceAccount)
F3(NetworkPolicy)
F4(MinIO Setup Job)
end
subgraph MinIO [Namespace: minio]
M1[(Shared MinIO S3)]
M2(MinIO Setup Job)
end
end
B -.-> D
C -.-> D
F4 -->|Creates Buckets & Credentials| M1
M2 -->|Creates Buckets & Credentials| M1
M2 -->|Provisions Scoped Secret| F
F1 -->|Reads/Writes| M1
```
---
## 2. Meeting the Requirements
## 2. Requirements
This section details how the technical choices address the specific requirements of the assessment.
@@ -69,9 +75,11 @@ values, and provisions the new namespace.
* **Network Security:** A `NetworkPolicy` is deployed within each tenant namespace
to restrict ingress/egress traffic. The Jupyter notebook can only communicate
with necessary services (like MinIO) and the internet, preventing cross-namespace communication.
* **Automated & Isolated S3 Setup:** Instead of sharing credentials, a one-off Kubernetes `Job`
runs during the Helm deployment for each tenant. This job connects to the MinIO admin API,
creates a reference bucket (Read-Only), a work bucket (Read/Write), and generates
**Automated & Isolated S3 Setup:** To prevent exposing admin credentials outside the storage
infrastructure, a Kubernetes Job runs directly within the minio namespace during tenant provisioning.
This job connects to the MinIO admin API locally, creates a reference bucket (Read-Only), a work bucket (Read/Write),
and generates dedicated, scoped S3 credentials. It then creates the resulting secret containing only
tenant-scoped credentials directly in the target tenant namespace.
**dedicated, scoped S3 credentials**. These credentials are saved as a Kubernetes
Secret locally in the tenant's namespace.
* **Security Context:** The Docker images use the `tini` init system and execute
@@ -80,8 +88,8 @@ application containers as a non-root user.
### 2.3 Container Images & Dependencies
* **Base & Dependent Images:** The project uses two custom images located in
`images/`. The `tenant-base` image contains the core OS (`python:3.12-slim-bookworm`),
security contexts, and user setups. The `tenant-jupyter` image builds on top
`images/`. The `tenant/base` image contains the core OS (`python:3.12-slim-bookworm`),
security contexts, and user setups. The `tenant/jupyter` image builds on top
of the base image and installs the application requirements.
* **No Hardcoded Secrets:** Images are completely stateless and free of secrets. They
rely solely on environment variables injected at runtime via Kubernetes Secrets.
@@ -90,7 +98,7 @@ rely solely on environment variables injected at runtime via Kubernetes Secrets.
* **Gitea Actions Pipeline:** The CI/CD workflow (`.gitea/workflows/`) automatically
manages the image lifecycle and code quality.
* **Validation:** Helm charts and Kubernetes manifests are validated before deployment.
* **Validation:** Helm charts and tenant configs are validated before deployment.
* **Ordered Build Strategy:** The pipeline respects the image hierarchy. It builds the base image first,
then uses it to build the Jupyter image. Triggered on `*.*.*` tags, it pushes the versioned
artifacts to a private Docker registry.
@@ -100,9 +108,10 @@ All infrastructure and application changes must be proposed via Pull Requests.
The CI pipelines act as mandatory status checks, ensuring that no code can be merged
into `main` unless the configurations are validated, no secrets are leaked, and all tests pass successfully.
* **Secret Scanning:** `TruffleHog` runs as a CI step to scan the repository and prevent any accidental commit of sensitive information (AWS keys, passwords).
* **Secret Injection:** Admin credentials (Docker registry, MinIO admin) are injected into the Argo CD
namespace and propagated to tenant namespaces dynamically matching `tenant-[a-z0-9-]+`
using the `Reflector` Kubernetes addon.
* **Secret Injection:** Docker registry credentials are injected into the argocd namespace and
propagated to tenant namespaces dynamically matching tenant-[a-z0-9-]+ using
the Reflector Kubernetes addon. MinIO admin credentials remain strictly contained
within the minio namespace and are never reflected to tenant environments.
---
@@ -111,6 +120,9 @@ using the `Reflector` Kubernetes addon.
This section provides the step-by-step instructions to set up the local Kubernetes cluster and
bootstrap the GitOps environment.
*Note: You will not be able to use this repo directly as it require credentials for
my selfhosted gitea and docker registry.*
### 3.1 Prerequisites
Ensure the following tools are installed on your machine (e.g., via Homebrew on macOS):
@@ -176,8 +188,8 @@ kubectl create secret docker-registry registry-credentials \
kubectl annotate secret registry-credentials -n argocd --overwrite \
reflector.v1.k8s.emberstack.com/reflection-allowed="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-enabled="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces="tenant-[a-z0-9-]+" \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces="tenant-[a-z0-9-]+"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces="tenant-[a-z0-9-]+,minio" \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces="tenant-[a-z0-9-]+,minio"
```
@@ -187,13 +199,7 @@ kubectl annotate secret registry-credentials -n argocd --overwrite \
kubectl create secret generic minio-admin-credentials \
--from-literal=MINIO_ROOT_USER=admin \
--from-literal=MINIO_ROOT_PASSWORD=password123 \
-n argocd
kubectl annotate secret minio-admin-credentials -n argocd --overwrite \
reflector.v1.k8s.emberstack.com/reflection-allowed="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-enabled="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces="tenant-[a-z0-9-]+" \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces="tenant-[a-z0-9-]+"
-n minio
```
+1 -3
View File
@@ -6,8 +6,6 @@ metadata:
labels:
app: jupyter
tenant: 'tenant-{{.Values.name}}'
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
replicas: 1
selector:
@@ -29,7 +27,7 @@ spec:
imagePullPolicy: IfNotPresent
envFrom:
- secretRef:
name: s3-credentials
name: 'tenant-{{.Values.name}}-s3-credentials'
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
+58 -63
View File
@@ -2,99 +2,94 @@ apiVersion: batch/v1
kind: Job
metadata:
name: 'tenant-{{ .Values.name }}-minio-setup'
namespace: {{ .Release.Namespace }}
namespace: minio
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
ttlSecondsAfterFinished: 100
template:
spec:
serviceAccountName: minio-provisioner
restartPolicy: OnFailure
imagePullSecrets:
- name: registry-credentials
containers:
- name: mc
image: minio/mc:RELEASE.2024-03-03T00-13-08Z
env:
# 1. MinIO Admin Credentials (Replicated from argocd namespace)
- name: MINIO_ADMIN_USER
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_USER
- name: MINIO_ADMIN_PASS
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_PASSWORD
# 2. Randomly Generated Tenant Credentials
- name: TENANT_USER
valueFrom:
secretKeyRef:
name: s3-credentials
key: MINIO_ACCESS_KEY_ID
- name: TENANT_PASS
valueFrom:
secretKeyRef:
name: s3-credentials
key: MINIO_SECRET_ACCESS_KEY
- name: BUCKET_REF
valueFrom:
secretKeyRef:
name: s3-credentials
key: BUCKET_REF
- name: BUCKET_WORK
valueFrom:
secretKeyRef:
name: s3-credentials
key: BUCKET_WORK
command:
- /bin/sh
- -c
- name: setup
image: registry.bouvais.lu/minio-runner:1.0.0
command: ["/bin/sh", "-c"]
args:
- |
# Fail immediately if any command fails
set -e
# Connect to MinIO using quotes to protect special characters in passwords
mc alias set minioadmin http://minio.minio.svc.cluster.local:9000 "$MINIO_ADMIN_USER" "$MINIO_ADMIN_PASS"
# Generate password
SCOPED_SECRET=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 32)
# Configure MinIO alias
mc alias set local http://minio.minio.svc:9000 \
"$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
# Create buckets
mc mb --ignore-existing minioadmin/$BUCKET_REF
mc mb --ignore-existing minioadmin/$BUCKET_WORK
mc mb -p local/ref-tenant-{{ .Values.name }} local/work-tenant-{{ .Values.name }}
echo "hello tenant-{{ .Values.name }}" | mc pipe minioadmin/$BUCKET_REF/hello.txt
# Create user with generated credentials
mc admin user add local tenant-{{ .Values.name }}-user "$SCOPED_SECRET"
# Create tenant user (REMOVED || true)
mc admin user add minioadmin "$TENANT_USER" "$TENANT_PASS"
# Add demo file
echo "hello tenant-{{ .Values.name }}" | mc pipe local/ref-tenant-{{ .Values.name }}/hello.txt
# Apply IAM isolation policy
# Inline policy generation to avoid missing mount issues
cat <<EOF > /tmp/policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyRefBucket",
"Effect": "Allow",
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::$BUCKET_REF",
"arn:aws:s3:::$BUCKET_WORK"
"arn:aws:s3:::ref-tenant-{{ .Values.name }}",
"arn:aws:s3:::ref-tenant-{{ .Values.name }}/*"
]
},
{
"Sid": "ReadWriteWorkBucket",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::$BUCKET_REF/*"]
},
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::$BUCKET_WORK/*"]
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::work-tenant-{{ .Values.name }}",
"arn:aws:s3:::work-tenant-{{ .Values.name }}/*"
]
}
]
}
EOF
# Apply policy and attach to user
mc admin policy create local tenant-{{ .Values.name }}-policy /tmp/policy.json
mc admin policy attach local tenant-{{ .Values.name }}-policy --user tenant-{{ .Values.name }}-user
mc admin policy create minioadmin tenant-{{ .Values.name }}-policy /tmp/policy.json || mc admin policy update minioadmin tenant-{{ .Values.name }}-policy /tmp/policy.json
# Good enough for now, would need better error handeling
mc admin policy attach minioadmin tenant-{{ .Values.name }}-policy --user="$TENANT_USER" || echo "Policy already attached, skipping."
echo "MinIO setup completed for tenant-{{ .Values.name }}"
# Create target secret with matching credentials
kubectl create secret generic tenant-{{ .Values.name }}-s3-credentials \
--from-literal=S3_ENDPOINT="http://minio.minio.svc.cluster.local:9000" \
--from-literal=MINIO_ACCESS_KEY_ID=tenant-{{ .Values.name }}-user \
--from-literal=MINIO_SECRET_ACCESS_KEY="$SCOPED_SECRET" \
--from-literal=BUCKET_REF="ref-tenant-{{ .Values.name }}" \
--from-literal=BUCKET_WORK="work-tenant-{{ .Values.name }}" \
-n minio --dry-run=client -o yaml | kubectl apply -f -
# Annotate for Emberstack Reflector
kubectl annotate secret tenant-{{ .Values.name }}-s3-credentials -n minio --overwrite \
reflector.v1.k8s.emberstack.com/reflection-allowed="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-enabled="true" \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces="tenant-{{ .Values.name }}" \
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces="tenant-{{ .Values.name }}"
envFrom:
- secretRef:
name: minio-admin-credentials
+3 -4
View File
@@ -15,15 +15,14 @@ spec:
- Ingress
- Egress
# Entrée : Autorise le trafic vers le Notebook Jupyter sur le port 8888
# Entrée
ingress:
- ports:
- protocol: TCP
port: 8888
# Sortie : Restreint les communications aux composants strictement nécessaires
# Sortie
egress:
# 1. Résolution DNS dans kube-system
- to:
- namespaceSelector:
matchLabels:
@@ -34,7 +33,7 @@ spec:
- protocol: TCP
port: 53
# 2. Flux vers l'API MinIO (Stockage S3)
# 2. Vers MinIO
- to:
- namespaceSelector:
matchLabels:
+2 -1
View File
@@ -10,7 +10,8 @@ type: Opaque
stringData:
MINIO_ACCESS_KEY_ID: 'tenant-{{ .Values.name }}-user'
# If the secret exist, keep. Otherwise create a new random one. (To prevent a new password at each sync).
MINIO_SECRET_ACCESS_KEY: {{ if and $existingSecret $existingSecret.data (hasKey $existingSecret.data "AWS_SECRET_ACCESS_KEY") }}{{ index $existingSecret.data "AWS_SECRET_ACCESS_KEY" | b64dec }}{{ else }}{{ randAlphaNum 24 }}{{ end }}
MINIO_SECRET_ACCESS_KEY: {{ if and $existingSecret $existingSecret.data (hasKey $existingSecret.data "MINIO_SECRET_ACCESS_KEY") }}{{ index $existingSecret.data "MINIO_SECRET_ACCESS_KEY" | b64dec }}{{ else }}{{ randAlphaNum 24 }}{{ end }}
S3_ENDPOINT: "http://minio.minio.svc.cluster.local:9000"
BUCKET_REF: "ref-tenant-{{ .Values.name }}"
BUCKET_WORK: "work-tenant-{{ .Values.name }}"
+1 -1
View File
@@ -8,7 +8,7 @@ spec:
source:
repoURL: https://git.bouvais.lu/adrien/ctie-exercice
targetRevision: main
path: gitops/tenants
path: gitops
directory:
recurse: true
destination:
+6
View File
@@ -4,12 +4,18 @@ metadata:
name: tenants
namespace: argocd
spec:
# Ensure only pull from here
sourceRepos:
- https://git.bouvais.lu/adrien/ctie-exercice
# So this namespace can only affect other tenant and minio (not kube-system or argocd)
destinations:
- server: https://kubernetes.default.svc
namespace: 'tenant-*'
- server: https://kubernetes.default.svc
namespace: 'minio'
# This is necessary to create namespace
clusterResourceWhitelist:
- group: ''
kind: Namespace
+13
View File
@@ -0,0 +1,13 @@
FROM alpine:3.20
RUN apk add --no-cache curl bash openssl \
&& curl -fsSL -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc \
&& chmod +x /usr/local/bin/mc \
&& curl -fsSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
&& chmod +x /usr/local/bin/kubectl \
&& adduser -D -u 1000 runner
USER runner
WORKDIR /home/runner
ENTRYPOINT ["/bin/bash"]
+31 -1
View File
@@ -1,3 +1,33 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: minio-provisioner
namespace: minio
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-writer
namespace: minio
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: minio-provisioner-secret-writer
namespace: minio
subjects:
- kind: ServiceAccount
name: minio-provisioner
namespace: minio
roleRef:
kind: Role
name: secret-writer
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -13,7 +43,7 @@ spec:
spec:
containers:
- name: minio
image: minio/minio:latest
image: quay.io/minio/minio:latest
args: ["server", "/data", "--console-address", ":9001"]
env:
- name: MINIO_ROOT_USER