platform: restore cert-manager and encrypt budget storage
This commit is contained in:
parent
71bab17665
commit
8192dfeebe
@ -15,6 +15,5 @@ spec:
|
||||
namespace: flux-system
|
||||
targetNamespace: cert-manager
|
||||
dependsOn:
|
||||
- name: cert-manager-cleanup
|
||||
- name: helm
|
||||
wait: true
|
||||
|
||||
@ -4,7 +4,6 @@ kind: Kustomization
|
||||
resources:
|
||||
- core/kustomization.yaml
|
||||
- helm/kustomization.yaml
|
||||
- cert-manager-cleanup/kustomization.yaml
|
||||
- cert-manager/kustomization.yaml
|
||||
- metallb/kustomization.yaml
|
||||
- traefik/kustomization.yaml
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
# infrastructure/modules/base/storageclass/asteria-encrypted.yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: asteria-encrypted
|
||||
parameters:
|
||||
diskSelector: asteria
|
||||
fromBackup: ""
|
||||
numberOfReplicas: "2"
|
||||
staleReplicaTimeout: "30"
|
||||
fsType: "ext4"
|
||||
replicaAutoBalance: "least-effort"
|
||||
dataLocality: "disabled"
|
||||
encrypted: "true"
|
||||
csi.storage.k8s.io/provisioner-secret-name: ${pvc.name}
|
||||
csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}
|
||||
csi.storage.k8s.io/node-publish-secret-name: ${pvc.name}
|
||||
csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace}
|
||||
csi.storage.k8s.io/node-stage-secret-name: ${pvc.name}
|
||||
csi.storage.k8s.io/node-stage-secret-namespace: ${pvc.namespace}
|
||||
provisioner: driver.longhorn.io
|
||||
reclaimPolicy: Retain
|
||||
allowVolumeExpansion: true
|
||||
volumeBindingMode: Immediate
|
||||
@ -3,4 +3,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- asteria.yaml
|
||||
- asteria-encrypted.yaml
|
||||
- astreae.yaml
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: actual-budget-data
|
||||
name: actual-budget-data-encrypted
|
||||
namespace: finance
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: asteria
|
||||
storageClassName: asteria-encrypted
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
||||
@ -169,7 +169,7 @@ spec:
|
||||
volumes:
|
||||
- name: actual-data
|
||||
persistentVolumeClaim:
|
||||
claimName: actual-budget-data
|
||||
claimName: actual-budget-data-encrypted
|
||||
- name: actual-openid-bootstrap-script
|
||||
configMap:
|
||||
name: actual-openid-bootstrap-script
|
||||
|
||||
@ -6,7 +6,7 @@ metadata:
|
||||
namespace: finance
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
spec:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: finance-secrets-ensure-4
|
||||
name: finance-secrets-ensure-5
|
||||
namespace: finance
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
|
||||
24
services/finance/finance-secrets-ensure-rbac.yaml
Normal file
24
services/finance/finance-secrets-ensure-rbac.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
# services/finance/finance-secrets-ensure-rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: finance-secrets-ensure
|
||||
namespace: finance
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "create"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: finance-secrets-ensure
|
||||
namespace: finance
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: finance-secrets-ensure
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: finance-secrets-ensure
|
||||
namespace: finance
|
||||
@ -6,7 +6,7 @@ metadata:
|
||||
namespace: finance
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
spec:
|
||||
|
||||
@ -6,6 +6,7 @@ resources:
|
||||
- namespace.yaml
|
||||
- serviceaccount.yaml
|
||||
- portal-rbac.yaml
|
||||
- finance-secrets-ensure-rbac.yaml
|
||||
- actual-budget-data-pvc.yaml
|
||||
- firefly-storage-pvc.yaml
|
||||
- finance-secrets-ensure-job.yaml
|
||||
|
||||
@ -3,6 +3,7 @@ import base64
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@ -20,18 +21,81 @@ def require_value(label: str, value: str) -> None:
|
||||
raise RuntimeError(f"missing {label}")
|
||||
|
||||
|
||||
def http_json(method: str, url: str, headers=None, payload=None):
|
||||
def http_json(method: str, url: str, headers=None, payload=None, context=None):
|
||||
data = None
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
with urllib.request.urlopen(req, timeout=15, context=context) as resp:
|
||||
body = resp.read()
|
||||
if not body:
|
||||
return resp.status, None
|
||||
return resp.status, json.loads(body.decode())
|
||||
|
||||
|
||||
def k8s_context() -> ssl.SSLContext:
|
||||
ca_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
|
||||
if ca_path.exists():
|
||||
return ssl.create_default_context(cafile=str(ca_path))
|
||||
return ssl.create_default_context()
|
||||
|
||||
|
||||
def k8s_api_url(path: str) -> str:
|
||||
host = os.environ.get("KUBERNETES_SERVICE_HOST")
|
||||
port = os.environ.get("KUBERNETES_SERVICE_PORT", "443")
|
||||
if not host:
|
||||
raise RuntimeError("missing kubernetes service host")
|
||||
return f"https://{host}:{port}{path}"
|
||||
|
||||
|
||||
def k8s_get_secret(namespace: str, name: str, token: str):
|
||||
try:
|
||||
_, body = http_json(
|
||||
"GET",
|
||||
k8s_api_url(f"/api/v1/namespaces/{namespace}/secrets/{name}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
context=k8s_context(),
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
return None
|
||||
raise
|
||||
return body
|
||||
|
||||
|
||||
def k8s_create_secret(namespace: str, name: str, token: str, string_data: dict):
|
||||
payload = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": {"name": name, "namespace": namespace},
|
||||
"type": "Opaque",
|
||||
"stringData": string_data,
|
||||
}
|
||||
try:
|
||||
status, _ = http_json(
|
||||
"POST",
|
||||
k8s_api_url(f"/api/v1/namespaces/{namespace}/secrets"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
payload=payload,
|
||||
context=k8s_context(),
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 409:
|
||||
return
|
||||
raise
|
||||
if status not in (200, 201):
|
||||
raise RuntimeError(f"k8s secret create failed for {name} (status {status})")
|
||||
|
||||
|
||||
def decode_secret_value(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
return base64.b64decode(value.encode()).decode("utf-8")
|
||||
|
||||
|
||||
def vault_login(vault_addr: str, role: str, jwt: str) -> str:
|
||||
status, body = http_json(
|
||||
"POST",
|
||||
@ -152,17 +216,63 @@ def ensure_actual_db(vault_addr: str, token: str):
|
||||
)
|
||||
|
||||
|
||||
def ensure_actual_encryption(vault_addr: str, token: str, sa_token: str):
|
||||
namespace = os.environ.get("FINANCE_NAMESPACE", "finance")
|
||||
secret_name = os.environ.get("ACTUAL_BUDGET_PVC_NAME", "actual-budget-data-encrypted")
|
||||
if not sa_token:
|
||||
raise RuntimeError("missing service account token for k8s")
|
||||
|
||||
vault_data = vault_read(vault_addr, token, "finance/actual-encryption")
|
||||
vault_key = vault_data.get("CRYPTO_KEY_VALUE", "")
|
||||
|
||||
k8s_secret = k8s_get_secret(namespace, secret_name, sa_token)
|
||||
k8s_key = ""
|
||||
if k8s_secret:
|
||||
data = k8s_secret.get("data", {}) or {}
|
||||
k8s_key = decode_secret_value(data.get("CRYPTO_KEY_VALUE", ""))
|
||||
|
||||
if vault_key and k8s_key and vault_key != k8s_key:
|
||||
raise RuntimeError("actual encryption key mismatch between vault and k8s")
|
||||
|
||||
key = vault_key or k8s_key
|
||||
provider = "secret"
|
||||
if not key:
|
||||
key = secrets.token_urlsafe(48)
|
||||
vault_write(
|
||||
vault_addr,
|
||||
token,
|
||||
"finance/actual-encryption",
|
||||
{"CRYPTO_KEY_VALUE": key, "CRYPTO_KEY_PROVIDER": provider},
|
||||
)
|
||||
elif not vault_key:
|
||||
vault_write(
|
||||
vault_addr,
|
||||
token,
|
||||
"finance/actual-encryption",
|
||||
{"CRYPTO_KEY_VALUE": key, "CRYPTO_KEY_PROVIDER": provider},
|
||||
)
|
||||
|
||||
if not k8s_secret:
|
||||
k8s_create_secret(
|
||||
namespace,
|
||||
secret_name,
|
||||
sa_token,
|
||||
{"CRYPTO_KEY_VALUE": key, "CRYPTO_KEY_PROVIDER": provider},
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
vault_addr = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200")
|
||||
vault_role = os.environ.get("VAULT_ROLE", "finance-secrets")
|
||||
jwt = read_file(Path("/var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||
if not jwt:
|
||||
sa_token = read_file(Path("/var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||
if not sa_token:
|
||||
raise RuntimeError("missing service account token")
|
||||
|
||||
token = vault_login(vault_addr, vault_role, jwt)
|
||||
token = vault_login(vault_addr, vault_role, sa_token)
|
||||
ensure_firefly_db(vault_addr, token)
|
||||
ensure_firefly_secrets(vault_addr, token)
|
||||
ensure_actual_db(vault_addr, token)
|
||||
ensure_actual_encryption(vault_addr, token, sa_token)
|
||||
print("finance secrets ensured")
|
||||
return 0
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user