Compare commits
2 Commits
9d5dd332fc
...
2e26b24e7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e26b24e7b | |||
| f2d60e0b15 |
55
.woodpecker/flux-reconcile-source.yaml
Normal file
55
.woodpecker/flux-reconcile-source.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: fresh-start
|
||||||
|
|
||||||
|
skip_clone: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get kubernetes access from OpenBao
|
||||||
|
image: quay.io/openbao/openbao:2.5.2
|
||||||
|
volumes:
|
||||||
|
- secrets:/secrets
|
||||||
|
environment:
|
||||||
|
ROLE_ID:
|
||||||
|
from_secret: flux_reconcile_role_id
|
||||||
|
SECRET_ID:
|
||||||
|
from_secret: flux_reconcile_secret_id
|
||||||
|
commands:
|
||||||
|
- bao write -field token auth/approle/login
|
||||||
|
role_id=$ROLE_ID
|
||||||
|
secret_id=$SECRET_ID
|
||||||
|
\> /secrets/.vault_id
|
||||||
|
- export VAULT_TOKEN=$(cat /secrets/.vault_id)
|
||||||
|
- bao write -format json /kubernetes/creds/flux-reconcile
|
||||||
|
\> /secrets/kube_credentials
|
||||||
|
- bao read -format
|
||||||
|
- name: Construct Kubeconfig
|
||||||
|
image: alpine/k8s:1.32.13
|
||||||
|
volumes:
|
||||||
|
- secrets:/secrets
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /secrets/kubeconfig
|
||||||
|
commands:
|
||||||
|
- kubectl config set-cluster cluster
|
||||||
|
--server=https://$KUBERNETES_SERVICE_HOST
|
||||||
|
--client-certificate=/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
- kubectl config set-credentials cluster
|
||||||
|
--token=$(jq -r .data.service_account_token /secrets/kube_credentials)
|
||||||
|
- kubectl config set-context cluster
|
||||||
|
--cluster cluster
|
||||||
|
--user cluster
|
||||||
|
--namespace flux-system
|
||||||
|
--current=true
|
||||||
|
- name: Reconcile git source
|
||||||
|
image: ghcr.io/fluxcd/flux-cli:v2.8.3
|
||||||
|
volumes:
|
||||||
|
- secrets:/secrets
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /secrets/kubeconfig
|
||||||
|
commands:
|
||||||
|
- flux reconcile source git flux-system
|
||||||
|
- name: Invalidate OpenBao token
|
||||||
|
image: quay.io/openbao/openbao:2.5.2
|
||||||
|
commands:
|
||||||
|
- export VAULT_TOKEN=$(cat /secrets/.vault_id)
|
||||||
|
- bao write auth/token/revoke-self
|
||||||
32
infra/configs/openbao-k8s-se-role.yaml
Normal file
32
infra/configs/openbao-k8s-se-role.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Roles with needed access for OpenBao's Kubernetes secret engine
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: k8s-full-secrets-abilities
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["namespaces"]
|
||||||
|
verbs: ["get"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["serviceaccounts", "serviceaccounts/token"]
|
||||||
|
verbs: ["create", "update", "delete"]
|
||||||
|
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||||
|
resources: ["rolebindings", "clusterrolebindings"]
|
||||||
|
verbs: ["create", "update", "delete"]
|
||||||
|
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||||
|
resources: ["roles", "clusterroles"]
|
||||||
|
verbs: ["bind", "escalate", "create", "update", "delete"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: openbao-token-creator-binding
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: k8s-full-secrets-abilities
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: openbao
|
||||||
|
namespace: openbao
|
||||||
@@ -25,3 +25,4 @@ resources:
|
|||||||
|
|
||||||
- configs/openbao-volume.yaml
|
- configs/openbao-volume.yaml
|
||||||
- controllers/openbao.yaml
|
- controllers/openbao.yaml
|
||||||
|
- configs/openbao-k8s-se-role.yaml
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import hvac
|
import hvac
|
||||||
@@ -42,7 +43,7 @@ def synchronize_auth_kubernetes_config(client: hvac.Client):
|
|||||||
def synchronize_kubernetes_roles(client: hvac.Client):
|
def synchronize_kubernetes_roles(client: hvac.Client):
|
||||||
kubernetes = Kubernetes(client.adapter)
|
kubernetes = Kubernetes(client.adapter)
|
||||||
|
|
||||||
policy_dir = os.path.join(os.path.dirname(__file__), '../vault/kubernetes-roles/')
|
policy_dir = os.path.join(os.path.dirname(__file__), '../vault/kubernetes-auth-roles/')
|
||||||
|
|
||||||
roles: dict[str, Any] = {} # pyright:ignore[reportExplicitAny]
|
roles: dict[str, Any] = {} # pyright:ignore[reportExplicitAny]
|
||||||
for filename in os.listdir(policy_dir):
|
for filename in os.listdir(policy_dir):
|
||||||
@@ -67,6 +68,69 @@ def synchronize_kubernetes_roles(client: hvac.Client):
|
|||||||
# Using write data instead of kubernetes.create_role, we can pass raw yaml
|
# Using write data instead of kubernetes.create_role, we can pass raw yaml
|
||||||
_ = client.write_data(f'/auth/kubernetes/role/{role_name}', data=role_content) # pyright:ignore[reportAny]
|
_ = client.write_data(f'/auth/kubernetes/role/{role_name}', data=role_content) # pyright:ignore[reportAny]
|
||||||
|
|
||||||
|
def synchronize_approle_auth(client: hvac.Client):
|
||||||
|
if client.sys.list_auth_methods().get('approle/') is None:
|
||||||
|
print('Enabling AppRole auth method')
|
||||||
|
client.sys.enable_auth_method('approle', 'AppRole authorization for CI')
|
||||||
|
|
||||||
|
roles_dir = pathlib.Path(__file__).parent.joinpath('../vault/approles/')
|
||||||
|
roles: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for filename in roles_dir.iterdir():
|
||||||
|
with filename.open('r') as f:
|
||||||
|
role = yaml.safe_load(f.read())
|
||||||
|
assert type(role) is dict
|
||||||
|
roles[filename.stem] = role
|
||||||
|
|
||||||
|
roles_on_vault: list[str] = []
|
||||||
|
roles_response = client.list("auth/approle/roles")
|
||||||
|
if roles_response is not None:
|
||||||
|
roles_on_vault = roles_response['data']['keys']
|
||||||
|
|
||||||
|
for role in roles_on_vault:
|
||||||
|
if role not in roles:
|
||||||
|
print(f'Deleting role: {role}')
|
||||||
|
client.delete(f'auth/approle/role/{role}')
|
||||||
|
|
||||||
|
for role_name, role_content in roles.items():
|
||||||
|
print(f'Updating role: {role_name}')
|
||||||
|
client.write_data(f'auth/approle/role/{role_name}', data=role_content)
|
||||||
|
|
||||||
|
def synchronize_kubernetes_secretengine(client: hvac.Client):
|
||||||
|
# Ensure kubernetes secret engine is enabled
|
||||||
|
if client.sys.list_mounted_secrets_engines().get('kubernetes/') is None:
|
||||||
|
print('Enabling kubernetes secret engine')
|
||||||
|
client.sys.enable_secrets_engine('kubernetes', 'kubernetes', 'Cluster access')
|
||||||
|
|
||||||
|
# Write empty config (all defaults, working on the same cluster)
|
||||||
|
client.write('kubernetes/config', None)
|
||||||
|
|
||||||
|
policy_dir = pathlib.Path(__file__).parent.joinpath('../vault/kubernetes-se-roles/')
|
||||||
|
roles: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for filename in policy_dir.iterdir():
|
||||||
|
with filename.open('r') as f:
|
||||||
|
role = yaml.safe_load(f.read())
|
||||||
|
assert type(role) is dict
|
||||||
|
# generated_role_rules must be json or yaml formatted string, convert it
|
||||||
|
if 'generated_role_rules' in role and type(role['generated_role_rules']) is not str:
|
||||||
|
role['generated_role_rules'] = yaml.safe_dump(role['generated_role_rules'])
|
||||||
|
roles[filename.stem] = role
|
||||||
|
|
||||||
|
roles_on_vault: list[str] = []
|
||||||
|
roles_response = client.list("kubernetes/roles")
|
||||||
|
if roles_response is not None:
|
||||||
|
roles_on_vault = roles_response['data']['keys']
|
||||||
|
|
||||||
|
for role in roles_on_vault:
|
||||||
|
if role not in roles:
|
||||||
|
print(f'Deleting role: {role}')
|
||||||
|
client.delete(f'kubernetes/roles/{role}')
|
||||||
|
|
||||||
|
for role_name, role_content in roles.items():
|
||||||
|
print(f'Updating role: {role_name}')
|
||||||
|
client.write_data(f'kubernetes/roles/{role_name}', data=role_content)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="synchronizeVault",
|
prog="synchronizeVault",
|
||||||
@@ -82,5 +146,11 @@ if __name__ == '__main__':
|
|||||||
print('Synchronizing kubernetes config')
|
print('Synchronizing kubernetes config')
|
||||||
synchronize_auth_kubernetes_config(client)
|
synchronize_auth_kubernetes_config(client)
|
||||||
|
|
||||||
print('Synchronizing kubernetes roles')
|
print('Synchronizing kubernetes auth roles')
|
||||||
synchronize_kubernetes_roles(client)
|
synchronize_kubernetes_roles(client)
|
||||||
|
|
||||||
|
print('Synchronizing AppRole auth method')
|
||||||
|
synchronize_approle_auth(client)
|
||||||
|
|
||||||
|
print('Synchronizing kubernetes secret engine')
|
||||||
|
synchronize_kubernetes_secretengine(client)
|
||||||
|
|||||||
4
vault/approles/ci-flux-reconcile.yaml
Normal file
4
vault/approles/ci-flux-reconcile.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
token_ttl: 20m
|
||||||
|
token_max_ttl: 20m
|
||||||
|
policies:
|
||||||
|
- flux-reconcile
|
||||||
6
vault/kubernetes-se-roles/flux-reconcile.yaml
Normal file
6
vault/kubernetes-se-roles/flux-reconcile.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
allowed_kubernetes_namespaces: flux-system
|
||||||
|
generated_role_rules:
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["kustomize.toolkit.fluxcd.io"]
|
||||||
|
resources: ["gitrepositories"]
|
||||||
|
verbs: ["update", "watch"]
|
||||||
0
vault/kubernetes-secretengine-config.yaml
Normal file
0
vault/kubernetes-secretengine-config.yaml
Normal file
3
vault/policy/flux-reconcile.hcl
Normal file
3
vault/policy/flux-reconcile.hcl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
path "kubernetes/creds/flux-reconcile" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user