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
|
namespace: flux-system
|
||||||
targetNamespace: cert-manager
|
targetNamespace: cert-manager
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- name: cert-manager-cleanup
|
|
||||||
- name: helm
|
- name: helm
|
||||||
wait: true
|
wait: true
|
||||||
|
|||||||
@ -4,7 +4,6 @@ kind: Kustomization
|
|||||||
resources:
|
resources:
|
||||||
- core/kustomization.yaml
|
- core/kustomization.yaml
|
||||||
- helm/kustomization.yaml
|
- helm/kustomization.yaml
|
||||||
- cert-manager-cleanup/kustomization.yaml
|
|
||||||
- cert-manager/kustomization.yaml
|
- cert-manager/kustomization.yaml
|
||||||
- metallb/kustomization.yaml
|
- metallb/kustomization.yaml
|
||||||
- traefik/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
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- asteria.yaml
|
- asteria.yaml
|
||||||
|
- asteria-encrypted.yaml
|
||||||
- astreae.yaml
|
- astreae.yaml
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: actual-budget-data
|
name: actual-budget-data-encrypted
|
||||||
namespace: finance
|
namespace: finance
|
||||||
spec:
|
spec:
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
storageClassName: asteria
|
storageClassName: asteria-encrypted
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 10Gi
|
storage: 10Gi
|
||||||
|
|||||||
@ -169,7 +169,7 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: actual-data
|
- name: actual-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: actual-budget-data
|
claimName: actual-budget-data-encrypted
|
||||||
- name: actual-openid-bootstrap-script
|
- name: actual-openid-bootstrap-script
|
||||||
configMap:
|
configMap:
|
||||||
name: actual-openid-bootstrap-script
|
name: actual-openid-bootstrap-script
|
||||||
|
|||||||
@ -6,7 +6,7 @@ metadata:
|
|||||||
namespace: finance
|
namespace: finance
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: traefik
|
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"
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata:
|
metadata:
|
||||||
name: finance-secrets-ensure-4
|
name: finance-secrets-ensure-5
|
||||||
namespace: finance
|
namespace: finance
|
||||||
spec:
|
spec:
|
||||||
backoffLimit: 1
|
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
|
namespace: finance
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: traefik
|
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"
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@ -6,6 +6,7 @@ resources:
|
|||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- serviceaccount.yaml
|
- serviceaccount.yaml
|
||||||
- portal-rbac.yaml
|
- portal-rbac.yaml
|
||||||
|
- finance-secrets-ensure-rbac.yaml
|
||||||
- actual-budget-data-pvc.yaml
|
- actual-budget-data-pvc.yaml
|
||||||
- firefly-storage-pvc.yaml
|
- firefly-storage-pvc.yaml
|
||||||
- finance-secrets-ensure-job.yaml
|
- finance-secrets-ensure-job.yaml
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@ -20,18 +21,81 @@ def require_value(label: str, value: str) -> None:
|
|||||||
raise RuntimeError(f"missing {label}")
|
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
|
data = None
|
||||||
if payload is not None:
|
if payload is not None:
|
||||||
data = json.dumps(payload).encode()
|
data = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
|
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()
|
body = resp.read()
|
||||||
if not body:
|
if not body:
|
||||||
return resp.status, None
|
return resp.status, None
|
||||||
return resp.status, json.loads(body.decode())
|
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:
|
def vault_login(vault_addr: str, role: str, jwt: str) -> str:
|
||||||
status, body = http_json(
|
status, body = http_json(
|
||||||
"POST",
|
"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:
|
def main() -> int:
|
||||||
vault_addr = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200")
|
vault_addr = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200")
|
||||||
vault_role = os.environ.get("VAULT_ROLE", "finance-secrets")
|
vault_role = os.environ.get("VAULT_ROLE", "finance-secrets")
|
||||||
jwt = read_file(Path("/var/run/secrets/kubernetes.io/serviceaccount/token"))
|
sa_token = read_file(Path("/var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||||
if not jwt:
|
if not sa_token:
|
||||||
raise RuntimeError("missing service account 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_db(vault_addr, token)
|
||||||
ensure_firefly_secrets(vault_addr, token)
|
ensure_firefly_secrets(vault_addr, token)
|
||||||
ensure_actual_db(vault_addr, token)
|
ensure_actual_db(vault_addr, token)
|
||||||
|
ensure_actual_encryption(vault_addr, token, sa_token)
|
||||||
print("finance secrets ensured")
|
print("finance secrets ensured")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user