This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
# Files in this list are expanded by Gitea when a new repository is created
|
||||||
|
# from this template repository via the web UI.
|
||||||
|
README.md
|
||||||
|
.sops.yaml
|
||||||
|
bootstrap/applicationset.yaml
|
||||||
|
manifest/base/kustomization.yaml
|
||||||
|
manifest/base/deployment.yaml
|
||||||
|
manifest/base/service.yaml
|
||||||
|
manifest/overlays/production/kustomization.yaml
|
||||||
|
manifest/overlays/production/ingressroute.yaml
|
||||||
|
manifest/overlays/production/storage/persistentvolumeclaim.yaml
|
||||||
|
manifest/overlays/production/storage/persistentvolume-nfs.yaml
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
name: Validate manifests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \
|
||||||
|
| tar xz -C /usr/local/bin
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||||
|
curl -fsSL https://github.com/getsops/sops/releases/latest/download/sops-v3.x.linux.amd64 \
|
||||||
|
-o /usr/local/bin/sops
|
||||||
|
chmod +x /usr/local/bin/sops
|
||||||
|
curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||||
|
-o /usr/local/bin/yq
|
||||||
|
chmod +x /usr/local/bin/yq
|
||||||
|
|
||||||
|
# Traefik IngressRoute is a CRD, so kubeconform needs an extra schema source.
|
||||||
|
mkdir -p .ci-schemas/traefik.io
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/traefik.io/ingressroute_v1alpha1.json \
|
||||||
|
-o .ci-schemas/traefik.io/ingressroute_v1alpha1.json
|
||||||
|
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/IngressRoute_v1alpha1.json
|
||||||
|
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/IngressRoute.json
|
||||||
|
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/ingressroute.json
|
||||||
|
|
||||||
|
# ArgoCD ApplicationSet is also a CRD. It stays dormant until bootstrap
|
||||||
|
# is enabled, but once enabled we validate it with an explicit schema.
|
||||||
|
mkdir -p .ci-schemas/argoproj.io
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/applicationset_v1alpha1.json \
|
||||||
|
-o .ci-schemas/argoproj.io/applicationset_v1alpha1.json
|
||||||
|
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/ApplicationSet_v1alpha1.json
|
||||||
|
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/ApplicationSet.json
|
||||||
|
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/applicationset.json
|
||||||
|
|
||||||
|
- name: Helm lint
|
||||||
|
run: |
|
||||||
|
found=0
|
||||||
|
|
||||||
|
for parent in helm custom-charts; do
|
||||||
|
[ -d "$parent" ] || continue
|
||||||
|
|
||||||
|
for chart in "$parent"/*; do
|
||||||
|
[ -d "$chart" ] || continue
|
||||||
|
found=1
|
||||||
|
echo "Linting $chart"
|
||||||
|
helm lint "$chart"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
echo "No Helm charts found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: kubeconform - raw YAML
|
||||||
|
run: |
|
||||||
|
bootstrap_enabled=false
|
||||||
|
if [ -f bootstrap/config.yaml ] && grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then
|
||||||
|
bootstrap_enabled=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t manifests < <(
|
||||||
|
find . -type f -name '*.yaml' \
|
||||||
|
! -path './.gitea/*' \
|
||||||
|
! -name '.sops.yaml' \
|
||||||
|
! -name '*.enc.yaml' \
|
||||||
|
! -name '*.secret.yaml' \
|
||||||
|
! -name '*kustomization.yaml' \
|
||||||
|
! -name 'secret-generator.yaml' \
|
||||||
|
! -path './bootstrap/config.yaml' \
|
||||||
|
| sort
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$bootstrap_enabled" != "true" ]; then
|
||||||
|
filtered=()
|
||||||
|
for manifest in "${manifests[@]}"; do
|
||||||
|
[ "$manifest" = "./bootstrap/applicationset.yaml" ] && continue
|
||||||
|
filtered+=("$manifest")
|
||||||
|
done
|
||||||
|
manifests=("${filtered[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#manifests[@]}" -eq 0 ]; then
|
||||||
|
echo "No manifests found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${manifests[@]}" \
|
||||||
|
| xargs kubeconform \
|
||||||
|
-strict \
|
||||||
|
-kubernetes-version 1.35.0 \
|
||||||
|
-schema-location default \
|
||||||
|
-schema-location '.ci-schemas/{{.Group}}/{{.ResourceKind}}{{.KindSuffix}}.json' \
|
||||||
|
-schema-location '.ci-schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
|
||||||
|
-summary
|
||||||
|
|
||||||
|
- name: SOPS - check no secret files committed unencrypted
|
||||||
|
run: |
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if ! grep -q 'sops:' "$file"; then
|
||||||
|
echo "ERROR: $file appears to be unencrypted"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done < <(
|
||||||
|
find . -type f \( -name '*.secret.yaml' -o -name '*.enc.yaml' \) | sort
|
||||||
|
)
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
|
|
||||||
|
- name: Check SOPS sync
|
||||||
|
env:
|
||||||
|
SOPS_SYNC_HMAC_KEY: ${{ secrets.SOPS_SYNC_HMAC_KEY }}
|
||||||
|
run: scripts/ci/check-sops-sync
|
||||||
|
|
||||||
|
- name: Apply bootstrap ApplicationSet
|
||||||
|
env:
|
||||||
|
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
|
||||||
|
run: |
|
||||||
|
if [ "${GITHUB_EVENT_NAME:-}" != "push" ] || [ "${GITHUB_REF:-}" != "refs/heads/main" ]; then
|
||||||
|
echo "Skipping bootstrap apply: only push events on main may apply"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f bootstrap/config.yaml ] || ! grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then
|
||||||
|
echo "Skipping bootstrap apply: bootstrap/config.yaml is not enabled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG_B64:-}" ]; then
|
||||||
|
echo "KUBECONFIG_B64 secret is required to apply bootstrap/applicationset.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsSL https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl \
|
||||||
|
-o /usr/local/bin/kubectl
|
||||||
|
chmod +x /usr/local/bin/kubectl
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.kube"
|
||||||
|
printf '%s' "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config"
|
||||||
|
|
||||||
|
kubectl apply -f bootstrap/applicationset.yaml
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Check SOPS sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-sops-sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y openssl
|
||||||
|
curl -fsSL https://github.com/getsops/sops/releases/latest/download/sops-v3.x.linux.amd64 \
|
||||||
|
-o /tmp/sops
|
||||||
|
sudo install -m 0755 /tmp/sops /usr/local/bin/sops
|
||||||
|
curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||||
|
-o /tmp/yq
|
||||||
|
sudo install -m 0755 /tmp/yq /usr/local/bin/yq
|
||||||
|
|
||||||
|
- name: Configure SOPS age key
|
||||||
|
if: ${{ secrets.SOPS_AGE_KEY != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p "${HOME}/.config/sops/age"
|
||||||
|
printf '%s' '${{ secrets.SOPS_AGE_KEY }}' > "${HOME}/.config/sops/age/keys.txt"
|
||||||
|
chmod 600 "${HOME}/.config/sops/age/keys.txt"
|
||||||
|
|
||||||
|
- name: Check SOPS sync
|
||||||
|
env:
|
||||||
|
SOPS_SYNC_HMAC_KEY: ${{ secrets.SOPS_SYNC_HMAC_KEY }}
|
||||||
|
run: scripts/ci/check-sops-sync
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Local plaintext twins for sync-managed secrets. Keep these untracked.
|
||||||
|
manifest/overlays/**/secret.yaml
|
||||||
|
manifest/components/**/secret.yaml
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Replace the age recipient below with the public key used by ArgoCD / ksops
|
||||||
|
# in the target environment before storing real secrets in this repository.
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: manifest/(overlays|components)/.*/.*\.enc\.yaml$
|
||||||
|
age: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||||
|
- path_regex: manifest/(overlays|components)/.*/secret\.secret\.yaml$
|
||||||
|
age: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# authentik
|
||||||
|
|
||||||
|
Kubernetes deployment source-of-truth repository for `authentik`.
|
||||||
|
|
||||||
|
This template is intended for applications deployed with ArgoCD through an
|
||||||
|
`ApplicationSet`. It uses a `base/` plus `overlays/` layout, stores runtime
|
||||||
|
secrets as SOPS-encrypted manifests, and includes a Gitea workflow that checks
|
||||||
|
Kubernetes YAML syntax on every push and pull request. It also ships with a
|
||||||
|
dormant bootstrap `ApplicationSet` that only becomes active after an explicit
|
||||||
|
enable change and a promotion into `main`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── .gitea/
|
||||||
|
│ ├── template
|
||||||
|
│ └── workflows/validate.yaml
|
||||||
|
├── .github/
|
||||||
|
│ └── workflows/check-sops-sync.yml
|
||||||
|
├── .sops.yaml
|
||||||
|
├── bootstrap/
|
||||||
|
│ ├── applicationset.yaml
|
||||||
|
│ └── config.yaml
|
||||||
|
├── manifest/
|
||||||
|
│ ├── base/
|
||||||
|
│ │ ├── deployment.yaml
|
||||||
|
│ │ ├── kustomization.yaml
|
||||||
|
│ │ ├── namespace.yaml
|
||||||
|
│ │ └── service.yaml
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── example-component/
|
||||||
|
│ └── overlays/
|
||||||
|
│ └── production/
|
||||||
|
│ ├── ingressroute.yaml
|
||||||
|
│ ├── kustomization.yaml
|
||||||
|
│ ├── secret-generator.yaml
|
||||||
|
│ ├── secret.secret.yaml
|
||||||
|
│ └── storage/
|
||||||
|
│ ├── persistentvolumeclaim.yaml
|
||||||
|
│ └── persistentvolume-nfs.yaml
|
||||||
|
└── scripts/
|
||||||
|
├── ci/check-sops-sync
|
||||||
|
├── git-hooks/pre-commit
|
||||||
|
├── install-git-hooks
|
||||||
|
└── lib/
|
||||||
|
├── sops-path-regexes
|
||||||
|
└── sops-sync-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## First edits
|
||||||
|
|
||||||
|
Update these files before the first deployment:
|
||||||
|
|
||||||
|
1. `manifest/base/deployment.yaml`
|
||||||
|
Set the container image, ports, probes, resource requests, and any required
|
||||||
|
environment variables.
|
||||||
|
2. `manifest/overlays/production/ingressroute.yaml`
|
||||||
|
Set the real hostname, entry point names, and middleware references if used.
|
||||||
|
3. `manifest/overlays/production/storage/persistentvolumeclaim.yaml`
|
||||||
|
Set the requested size, access mode, and storage class for the environment.
|
||||||
|
4. `.sops.yaml`
|
||||||
|
Replace the sample age recipient with the public key that ArgoCD should use
|
||||||
|
for decryption.
|
||||||
|
5. `manifest/overlays/production/secret.secret.yaml`
|
||||||
|
Re-encrypt the placeholder secret values with your own data.
|
||||||
|
|
||||||
|
## Bootstrap activation model
|
||||||
|
|
||||||
|
This template is designed to start on an `init` branch. The intended flow is:
|
||||||
|
|
||||||
|
1. Create the repository from this template with `init` as the initial branch.
|
||||||
|
2. Tailor the manifests, secrets, storage, and ingress on `init`.
|
||||||
|
3. Keep `bootstrap/config.yaml` set to `enabled: false` while the repository is
|
||||||
|
being prepared.
|
||||||
|
4. Create `main` by merging `init` only when the repository is ready to be part
|
||||||
|
of the delivery pipeline.
|
||||||
|
5. Deliberately change `bootstrap/config.yaml` to `enabled: true` on `main` to
|
||||||
|
allow the workflow to apply the bootstrap `ApplicationSet`.
|
||||||
|
|
||||||
|
The workflow behavior is:
|
||||||
|
|
||||||
|
- `bootstrap/applicationset.yaml` is ignored by validation while bootstrap is disabled
|
||||||
|
- no cluster apply is attempted unless the workflow runs on `main`
|
||||||
|
- cluster apply also requires the `KUBECONFIG_B64` Gitea secret
|
||||||
|
|
||||||
|
## Bootstrap files
|
||||||
|
|
||||||
|
- `bootstrap/config.yaml`
|
||||||
|
Repository-local activation switch. Default is disabled.
|
||||||
|
- `bootstrap/applicationset.yaml`
|
||||||
|
ArgoCD `ApplicationSet` manifest template for this repository.
|
||||||
|
|
||||||
|
If the repository is created by copying files with plain Git instead of Gitea's
|
||||||
|
template expansion, you must replace the `${REPO_NAME...}` placeholders in
|
||||||
|
`bootstrap/applicationset.yaml` before enabling bootstrap.
|
||||||
|
|
||||||
|
## Storage model
|
||||||
|
|
||||||
|
Storage is overlay-owned, so each environment can request different backing
|
||||||
|
storage without sharing claims across environments.
|
||||||
|
|
||||||
|
- `manifest/overlays/production/storage/persistentvolumeclaim.yaml`
|
||||||
|
Default writable application storage. With the default `longhorn`
|
||||||
|
`storageClassName`, Kubernetes dynamically provisions the backing PV when the
|
||||||
|
claim is created.
|
||||||
|
- `manifest/overlays/production/storage/persistentvolume-nfs.yaml`
|
||||||
|
Optional static PV example for NFS-backed or pre-provisioned shared storage.
|
||||||
|
This file is not referenced by default. Add it to the overlay kustomization
|
||||||
|
only when you need a static PV.
|
||||||
|
|
||||||
|
The base deployment mounts the claim at `/data`.
|
||||||
|
|
||||||
|
If storage should be shared between multiple applications in different
|
||||||
|
repositories, keep those applications in the same namespace and have a single
|
||||||
|
repository own the PVC resource. The other repositories should reference the
|
||||||
|
same claim name from their deployments instead of creating a duplicate PVC.
|
||||||
|
|
||||||
|
## Secret workflow
|
||||||
|
|
||||||
|
1. Generate an age key pair.
|
||||||
|
2. Store the private key where ArgoCD / ksops can read it.
|
||||||
|
3. Update `.sops.yaml` with the matching public recipient.
|
||||||
|
4. Re-encrypt `manifest/overlays/production/secret.secret.yaml`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
age-keygen -o age-key.txt
|
||||||
|
sops --encrypt --in-place manifest/overlays/production/secret.secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repo-managed SOPS sync
|
||||||
|
|
||||||
|
This repo also supports sync-managed secrets using the naming convention:
|
||||||
|
|
||||||
|
```text
|
||||||
|
foo.yaml -> foo.enc.yaml -> foo.enc.yaml.sync-hmac
|
||||||
|
```
|
||||||
|
|
||||||
|
- `foo.yaml`
|
||||||
|
Local plaintext source. Keep it gitignored and untracked.
|
||||||
|
- `foo.enc.yaml`
|
||||||
|
Tracked SOPS-encrypted file.
|
||||||
|
- `foo.enc.yaml.sync-hmac`
|
||||||
|
Tracked HMAC sidecar used to prove the encrypted file still matches the last
|
||||||
|
synced plaintext content.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- `SOPS_SYNC_HMAC_KEY` must be set locally for hooks and in CI
|
||||||
|
- `sops`, `openssl`, and either `yq` or `python3` with `PyYAML` must be available
|
||||||
|
- CI also needs access to the repo's SOPS decryption mechanism, such as an age key
|
||||||
|
|
||||||
|
Generate a local HMAC key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Set it for the current shell session:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SOPS_SYNC_HMAC_KEY='<paste-generated-key-here>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Persist it in `zsh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "export SOPS_SYNC_HMAC_KEY='<paste-generated-key-here>'" >> ~/.zshrc
|
||||||
|
source ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Persist it in `bash`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "export SOPS_SYNC_HMAC_KEY='<paste-generated-key-here>'" >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify it is set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printenv SOPS_SYNC_HMAC_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the local hook:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/install-git-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Local workflow:
|
||||||
|
|
||||||
|
1. Keep your plaintext secret local-only, for example `manifest/overlays/production/secret.yaml`.
|
||||||
|
2. Stage either the plaintext file or its `.enc.yaml` twin.
|
||||||
|
3. On pre-commit, the hook encrypts the plaintext if needed, updates the
|
||||||
|
`.sync-hmac` sidecar, and stages both tracked files.
|
||||||
|
|
||||||
|
CI behavior:
|
||||||
|
|
||||||
|
- CI fails if a tracked `.enc.yaml` file is not valid SOPS content
|
||||||
|
- CI fails if the `.sync-hmac` sidecar is missing
|
||||||
|
- CI fails if decrypted plaintext no longer matches the tracked sidecar HMAC
|
||||||
|
|
||||||
|
Security tradeoffs:
|
||||||
|
|
||||||
|
- The sidecar is an HMAC, not a plain checksum, so the plaintext fingerprint is
|
||||||
|
keyed and not directly reusable without `SOPS_SYNC_HMAC_KEY`
|
||||||
|
- Anyone who can decrypt the repo secret files and also access the HMAC key can
|
||||||
|
recompute sidecars, so protect both inputs appropriately
|
||||||
|
- Plaintext local files must remain gitignored and never be committed
|
||||||
|
|
||||||
|
## ArgoCD ApplicationSet path
|
||||||
|
|
||||||
|
Point the generated ArgoCD `Application` at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
manifest/overlays/production
|
||||||
|
```
|
||||||
|
|
||||||
|
If your `ApplicationSet` uses repository scanning, this repository is designed
|
||||||
|
to be the single source of truth for one application, with ArgoCD rendering the
|
||||||
|
selected overlay.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
goTemplate: true
|
||||||
|
goTemplateOptions:
|
||||||
|
- missingkey=error
|
||||||
|
generators:
|
||||||
|
- list:
|
||||||
|
elements:
|
||||||
|
- environment: production
|
||||||
|
namespace: security
|
||||||
|
path: manifest/overlays/production
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: 'authentik-{{ .environment }}'
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: argocd
|
||||||
|
app.kubernetes.io/name: authentik
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-ha-http.apps:3000/olb42/authentik.git
|
||||||
|
targetRevision: main
|
||||||
|
path: '{{ .path }}'
|
||||||
|
plugin:
|
||||||
|
name: ksops
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: '{{ .namespace }}'
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=false
|
||||||
|
- ApplyOutOfSyncOnly=true
|
||||||
|
- ServerSideApply=true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
enabled: false
|
||||||
|
target_namespace: argocd
|
||||||
|
apply_from_branch: main
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: authentik-server
|
||||||
|
namespace: security
|
||||||
|
labels:
|
||||||
|
app: authentik-server
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: authentik-server
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: authentik-server
|
||||||
|
app.kubernetes.io/name: authentik-server
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 988
|
||||||
|
containers:
|
||||||
|
- name: server
|
||||||
|
image: ghcr.io/goauthentik/server:2026.2
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- server
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 9000
|
||||||
|
- name: https
|
||||||
|
containerPort: 9443
|
||||||
|
env:
|
||||||
|
- name: AUTHENTIK_LISTEN__HTTP
|
||||||
|
value: 0.0.0.0:9000
|
||||||
|
- name: AUTHENTIK_LISTEN__HTTPS
|
||||||
|
value: 0.0.0.0:9443
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||||
|
value: shared-postgres-rw.data.svc.cluster.local
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||||
|
value: authentik
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__USER
|
||||||
|
value: authentik
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__PORT
|
||||||
|
value: "5432"
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__SSLMODE
|
||||||
|
value: disable
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||||
|
value: file:///run/secrets/db_password
|
||||||
|
- name: AUTHENTIK_SECRET_KEY
|
||||||
|
value: file:///run/secrets/secret_key
|
||||||
|
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
||||||
|
value: "false"
|
||||||
|
volumeMounts:
|
||||||
|
- name: secrets
|
||||||
|
mountPath: /run/secrets/db_password
|
||||||
|
readOnly: true
|
||||||
|
subPath: db_password
|
||||||
|
- name: secrets
|
||||||
|
mountPath: /run/secrets/secret_key
|
||||||
|
readOnly: true
|
||||||
|
subPath: secret_key
|
||||||
|
- name: authentik-data
|
||||||
|
mountPath: /data
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /-/health/ready/
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /-/health/live/
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: secrets
|
||||||
|
secret:
|
||||||
|
secretName: authentik-secrets
|
||||||
|
- name: authentik-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: authentik-data
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: security
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- serviceaccount.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- worker-deployment.yaml
|
||||||
|
- service.yaml
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
|
namespace: security
|
||||||
|
labels:
|
||||||
|
app: authentik-server
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: authentik-server
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 9000
|
||||||
|
targetPort: http
|
||||||
|
- name: https
|
||||||
|
port: 9443
|
||||||
|
targetPort: https
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: authentik-worker
|
||||||
|
namespace: security
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: authentik-worker
|
||||||
|
namespace: security
|
||||||
|
labels:
|
||||||
|
app: authentik-worker
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: authentik-worker
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: authentik-worker
|
||||||
|
spec:
|
||||||
|
serviceAccountName: authentik-worker
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 988
|
||||||
|
containers:
|
||||||
|
- name: worker
|
||||||
|
image: ghcr.io/goauthentik/server:2026.2
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- worker
|
||||||
|
env:
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||||
|
value: shared-postgres-rw.data.svc.cluster.local
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||||
|
value: authentik
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__USER
|
||||||
|
value: authentik
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__PORT
|
||||||
|
value: "5432"
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__SSLMODE
|
||||||
|
value: disable
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||||
|
value: file:///run/secrets/db_password
|
||||||
|
- name: AUTHENTIK_SECRET_KEY
|
||||||
|
value: file:///run/secrets/secret_key
|
||||||
|
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: AUTHENTIK_WORKER__THREADS
|
||||||
|
value: "2"
|
||||||
|
volumeMounts:
|
||||||
|
- name: secrets
|
||||||
|
mountPath: /run/secrets/db_password
|
||||||
|
readOnly: true
|
||||||
|
subPath: db_password
|
||||||
|
- name: secrets
|
||||||
|
mountPath: /run/secrets/secret_key
|
||||||
|
readOnly: true
|
||||||
|
subPath: secret_key
|
||||||
|
- name: authentik-data
|
||||||
|
mountPath: /data
|
||||||
|
volumes:
|
||||||
|
- name: secrets
|
||||||
|
secret:
|
||||||
|
secretName: authentik-secrets
|
||||||
|
- name: authentik-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: authentik-data
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# +argocd:skip-file-rendering
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- files:
|
||||||
|
- example-test.env
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# +argocd:skip-file-rendering
|
||||||
|
|
||||||
|
test=works
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Database
|
||||||
|
metadata:
|
||||||
|
name: shared-postgres-authentik-db
|
||||||
|
namespace: data
|
||||||
|
spec:
|
||||||
|
name: authentik
|
||||||
|
owner: authentik
|
||||||
|
cluster:
|
||||||
|
name: shared-postgres
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
|
namespace: security
|
||||||
|
annotations:
|
||||||
|
gethomepage.dev/app: authentik-server
|
||||||
|
gethomepage.dev/enabled: "false"
|
||||||
|
gethomepage.dev/group: Security
|
||||||
|
gethomepage.dev/href: https://auth.olb42.com/
|
||||||
|
gethomepage.dev/icon: authentik
|
||||||
|
gethomepage.dev/name: Authentik
|
||||||
|
gethomepage.dev/namespace: security
|
||||||
|
gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_AUTHENTIK_KEY}}"
|
||||||
|
gethomepage.dev/widget.type: authentik
|
||||||
|
gethomepage.dev/widget.url: http://authentik.security:9000
|
||||||
|
gethomepage.dev/widget.version: "2"
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`auth.olb42.com`)
|
||||||
|
middlewares:
|
||||||
|
- name: lan-only
|
||||||
|
namespace: infra
|
||||||
|
services:
|
||||||
|
- name: authentik
|
||||||
|
port: 9000
|
||||||
|
tls:
|
||||||
|
secretName: olb42-wildcard-tls
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: security
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
- database.yaml
|
||||||
|
- ingressroute.yaml
|
||||||
|
- storage/persistentvolumeclaim.yaml
|
||||||
|
|
||||||
|
generators:
|
||||||
|
- secret-generator.yaml
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: viaduct.ai/v1
|
||||||
|
kind: ksops
|
||||||
|
metadata:
|
||||||
|
name: authentik-secret-generator
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/function: |
|
||||||
|
exec:
|
||||||
|
path: ksops
|
||||||
|
files:
|
||||||
|
- ./secret.secret.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:nhU=,iv:+igd9RTOMy7mwVDjx05WQIdytoCrWLjcnP2Wq8xBS1s=,tag:61hSvgd/g88pjdhPW6OQsg==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:T9Wrg72s,iv:1+cSHl0Dwwc1ddnw4dQI26r8/aEfMAPpsmz6BxxetrQ=,tag:y8HSeCa9gl7i+rW1Qpzc0Q==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:VNJIfO/EtW0Pd3xVKG2R1VM=,iv:53UVCYVV07Qdlbq7j9qghT/DHqv+98muUFpapoD7VBs=,tag:hKBPwm7dudPUlRCgp4D9/g==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:/STezL4E9q4=,iv:mImCvy0lheb1px4+VEm7Z2c6qgQFerRnciPUy+arG2o=,tag:ZJxPwZ7pO6j+8xgHSDK/Og==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:ypwwk80m,iv:gbfQojFcmmGCtALVkCcICc4Z4dDXQK8clTGIMkOMOkY=,tag:Y0AskWriaazAlDkNN6Ufuw==,type:str]
|
||||||
|
stringData:
|
||||||
|
db_password: ENC[AES256_GCM,data:vx+4cvOPHycDmgp3fhTqsRC6ZjdviJy3svt5Kuhtzg48l8DNlxQpTvLgtdGYZmGS,iv:k84Nwxnuyc3D9ceXuMPhSTwzRrCucyUpJ/b7lrUTBlI=,tag:JOMAcJN9frgVkpeV8pm39A==,type:str]
|
||||||
|
secret_key: ENC[AES256_GCM,data:ZhH2xkHf/wLXiooCBP+9ZctsGSOMW1de4xz8IGPS0z1GSTReM9h7sSHhw1vJl7iHsr8ne4ehXQYvLd/QcvLVShS2WVDSt1wXcKejnHgRTBk=,iv:xFSp0TRzBl0c4hx6UPv2CdFx9fMQ/oAes8JpWf1Zj7U=,tag:j8jrfilCe2gfyQr1kls4WQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- recipient: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSdWlWdTRWOWhTNnFocWlI
|
||||||
|
QTlLNmJtUkZLSUNOdi9yQmNVQkVkQi94WFc4Cno2NjJCeURlc3FZd0hTK0IzUXJq
|
||||||
|
STZtU0pBZ25SaFJqMFZNaE41a1p0SkEKLS0tIFg2VlphajFlSjdtSFM1NTVZdjZm
|
||||||
|
SmVrN2pFQ0MzVS9aUUtrVnFUaUd6TzAKksOI8fvGkE8/Qmk8yT7jvvakFWSLDYKa
|
||||||
|
7/pcTR0msB9P8zowsfe4pL+BJIObctXpyTRPCyyJg7/vOHqhuUv9Wg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2026-04-19T23:31:55Z"
|
||||||
|
mac: ENC[AES256_GCM,data:sRk5KdZXohSwRWs6jP/jUA1OMBgnxKQu82Wp2w/4q1K0XXe1KRSDyiIIklgdK6JAW5U0291EzKLg8KgTks3YsibteA4L7eFbwFDTXOn7OJwjhKlzTZXqxEjmGHmsKLa7v8Y8nKuJqn2CUrfUuLzyvUaQN2tXfigq/DPC0HyROQs=,iv:zzWBrUcOumbaDArX9xRPzz3J98fA9wAgC1cKafejWT8=,tag:DL9o2cA5Uslq6WWGzlt8QA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.12.2
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Optional static PV example for NFS-backed storage.
|
||||||
|
# This file is not referenced from kustomization.yaml by default because the
|
||||||
|
# default storage flow uses dynamic provisioning via the Longhorn PVC.
|
||||||
|
#
|
||||||
|
# To use this file:
|
||||||
|
# 1. Add storage/persistentvolume-nfs.yaml to resources in this overlay.
|
||||||
|
# 2. Update persistentvolumeclaim.yaml to set:
|
||||||
|
# storageClassName: nfs-shared
|
||||||
|
# volumeName: app-data
|
||||||
|
# accessModes: [ReadWriteMany]
|
||||||
|
# 3. Replace the placeholder NFS server and export path below.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: app-data
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
claimRef:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
name: app-data
|
||||||
|
namespace: security
|
||||||
|
storageClassName: nfs-shared
|
||||||
|
mountOptions:
|
||||||
|
- nfsvers=4.1
|
||||||
|
nfs:
|
||||||
|
server: nfs.example.internal
|
||||||
|
path: /exports/app-data
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: authentik-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
for cmd in git sops openssl; do
|
||||||
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||||
|
echo "check-sops-sync: required command missing: ${cmd}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||||
|
echo "check-sops-sync: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel)
|
||||||
|
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||||
|
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||||
|
|
||||||
|
if [ ! -x "${regex_script}" ]; then
|
||||||
|
echo "check-sops-sync: missing helper ${regex_script}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${lib_script}" ]; then
|
||||||
|
echo "check-sops-sync: missing helper ${lib_script}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
. "${lib_script}"
|
||||||
|
|
||||||
|
regexes=("${(@f)$("${regex_script}")}")
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
while IFS= read -r tracked_path; do
|
||||||
|
[ -n "${tracked_path}" ] || continue
|
||||||
|
[[ "${tracked_path}" == *.enc.* ]] || continue
|
||||||
|
|
||||||
|
if ! matches_any_rule "${tracked_path}" "${regexes[@]}"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
sidecar_path=$(hmac_sidecar_for_enc "${tracked_path}")
|
||||||
|
|
||||||
|
if ! git ls-files --error-unmatch -- "${sidecar_path}" >/dev/null 2>&1; then
|
||||||
|
echo "check-sops-sync: missing sync sidecar for ${tracked_path}" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_sops_encrypted_file "${tracked_path}"; then
|
||||||
|
echo "check-sops-sync: file is not valid SOPS-encrypted content: ${tracked_path}" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
sidecar_value=$(read_sidecar "${sidecar_path}" || true)
|
||||||
|
if [ -z "${sidecar_value}" ]; then
|
||||||
|
echo "check-sops-sync: sidecar is empty: ${sidecar_path}" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! computed_hmac=$(decrypt_enc_to_plain "${tracked_path}" | compute_hmac_for_stdin); then
|
||||||
|
echo "check-sops-sync: failed to decrypt ${tracked_path}" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${computed_hmac}" != "${sidecar_value}" ]; then
|
||||||
|
echo "check-sops-sync: decrypted plaintext HMAC does not match sidecar for ${tracked_path}" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done < <(git ls-files)
|
||||||
|
|
||||||
|
exit "${fail}"
|
||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
for cmd in git sops openssl; do
|
||||||
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: required command missing: ${cmd}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||||
|
echo "pre-commit: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel)
|
||||||
|
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||||
|
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||||
|
|
||||||
|
if [ ! -x "${regex_script}" ]; then
|
||||||
|
echo "pre-commit: missing helper ${regex_script}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${lib_script}" ]; then
|
||||||
|
echo "pre-commit: missing helper ${lib_script}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
. "${lib_script}"
|
||||||
|
|
||||||
|
regexes=("${(@f)$("${regex_script}")}")
|
||||||
|
typeset -A candidates
|
||||||
|
|
||||||
|
while IFS= read -r staged_path; do
|
||||||
|
[ -n "${staged_path}" ] || continue
|
||||||
|
|
||||||
|
candidate_enc=""
|
||||||
|
if [[ "${staged_path}" == *.enc.yaml ]]; then
|
||||||
|
candidate_enc="${staged_path}"
|
||||||
|
elif [[ "${staged_path}" == *.yaml ]]; then
|
||||||
|
if git ls-files --error-unmatch -- "${staged_path}" >/dev/null 2>&1; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
candidate_enc=$(plain_to_enc "${staged_path}") || continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "${candidate_enc}" ] || continue
|
||||||
|
if matches_any_rule "${candidate_enc}" "${regexes[@]}"; then
|
||||||
|
candidates["${candidate_enc}"]=1
|
||||||
|
fi
|
||||||
|
done < <(git diff --cached --name-only --diff-filter=ACMR)
|
||||||
|
|
||||||
|
for enc_path in "${(@k)candidates}"; do
|
||||||
|
plain_path=$(enc_to_plain "${enc_path}") || continue
|
||||||
|
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||||
|
|
||||||
|
if [ ! -f "${plain_path}" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_unstaged_changes "${plain_path}"; then
|
||||||
|
echo "pre-commit: plaintext file has unstaged changes: ${plain_path}" >&2
|
||||||
|
echo "pre-commit: stage or revert the plaintext change before committing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_hmac=$(compute_hmac_for_file "${plain_path}")
|
||||||
|
tracked_hmac=""
|
||||||
|
if [ -f "${sidecar_path}" ]; then
|
||||||
|
tracked_hmac=$(read_sidecar "${sidecar_path}" || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${current_hmac}" = "${tracked_hmac}" ] && [ -f "${enc_path}" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
encrypt_plain_to_enc "${plain_path}" "${enc_path}"
|
||||||
|
write_sidecar "${sidecar_path}" "${current_hmac}"
|
||||||
|
git add "${enc_path}" "${sidecar_path}"
|
||||||
|
done
|
||||||
|
|
||||||
|
for enc_path in "${(@k)candidates}"; do
|
||||||
|
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||||
|
|
||||||
|
if ! git cat-file -e ":${enc_path}" 2>/dev/null; then
|
||||||
|
echo "pre-commit: staged encrypted file missing: ${enc_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git cat-file -e ":${sidecar_path}" 2>/dev/null; then
|
||||||
|
echo "pre-commit: staged sync sidecar missing: ${sidecar_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_enc=$(mktemp)
|
||||||
|
trap 'rm -f "${tmp_enc}"' EXIT INT TERM
|
||||||
|
git show ":${enc_path}" > "${tmp_enc}"
|
||||||
|
|
||||||
|
staged_hmac=$(git show ":${sidecar_path}" | tr -d '\r\n')
|
||||||
|
computed_hmac=$(decrypt_enc_to_plain "${tmp_enc}" | compute_hmac_for_stdin)
|
||||||
|
|
||||||
|
rm -f "${tmp_enc}"
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
|
||||||
|
if [ "${computed_hmac}" != "${staged_hmac}" ]; then
|
||||||
|
echo "pre-commit: staged encrypted file and sidecar are out of sync: ${enc_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel)
|
||||||
|
hook_path="${repo_root}/.git/hooks/pre-commit"
|
||||||
|
target="${repo_root}/scripts/git-hooks/pre-commit"
|
||||||
|
|
||||||
|
mkdir -p "${repo_root}/.git/hooks"
|
||||||
|
cat > "${hook_path}" <<EOF
|
||||||
|
#!/usr/bin/env zsh
|
||||||
|
exec "${target}" "\$@"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "${hook_path}"
|
||||||
|
echo "Installed git hook: ${hook_path}"
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||||
|
sops_file="${repo_root}/.sops.yaml"
|
||||||
|
|
||||||
|
if [ ! -f "${sops_file}" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v yq >/dev/null 2>&1; then
|
||||||
|
yq -r '.creation_rules[]?.path_regex | select(. != null)' "${sops_file}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 - "${sops_file}" <<'PY'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except Exception as exc:
|
||||||
|
raise SystemExit(f"python3 fallback requires PyYAML: {exc}")
|
||||||
|
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||||
|
data = yaml.safe_load(handle) or {}
|
||||||
|
|
||||||
|
for rule in data.get("creation_rules") or []:
|
||||||
|
regex = rule.get("path_regex")
|
||||||
|
if regex:
|
||||||
|
print(regex)
|
||||||
|
PY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Unable to read .sops.yaml path_regex values: install yq or python3 with PyYAML" >&2
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
matches_any_rule() {
|
||||||
|
local path="$1"
|
||||||
|
shift
|
||||||
|
local regex
|
||||||
|
for regex in "$@"; do
|
||||||
|
[[ -n "${regex}" ]] || continue
|
||||||
|
if [[ "${path}" =~ ${regex} ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
enc_to_plain() {
|
||||||
|
local enc="$1"
|
||||||
|
[[ "${enc}" == *.enc.yaml ]] || return 1
|
||||||
|
print -r -- "${enc%.enc.yaml}.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
plain_to_enc() {
|
||||||
|
local plain="$1"
|
||||||
|
[[ "${plain}" == *.yaml ]] || return 1
|
||||||
|
if [[ "${plain}" == *.enc.yaml ]]; then
|
||||||
|
print -r -- "${plain}"
|
||||||
|
else
|
||||||
|
print -r -- "${plain%.yaml}.enc.yaml"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
hmac_sidecar_for_enc() {
|
||||||
|
local enc="$1"
|
||||||
|
print -r -- "${enc}.sync-hmac"
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt_plain_to_enc() {
|
||||||
|
local plain="$1"
|
||||||
|
local enc="$2"
|
||||||
|
sops --encrypt --input-type yaml --output-type yaml --output "${enc}" "${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt_enc_to_plain() {
|
||||||
|
local enc="$1"
|
||||||
|
sops --decrypt "${enc}"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_hmac_for_file() {
|
||||||
|
local path="$1"
|
||||||
|
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" "${path}" | awk '{print $NF}'
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_hmac_for_stdin() {
|
||||||
|
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" | awk '{print $NF}'
|
||||||
|
}
|
||||||
|
|
||||||
|
read_sidecar() {
|
||||||
|
local sidecar="$1"
|
||||||
|
[ -f "${sidecar}" ] || return 1
|
||||||
|
tr -d '\r\n' < "${sidecar}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_sidecar() {
|
||||||
|
local sidecar="$1"
|
||||||
|
local value="$2"
|
||||||
|
print -r -- "${value}" > "${sidecar}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_sops_encrypted_file() {
|
||||||
|
local path="$1"
|
||||||
|
[ -f "${path}" ] || return 1
|
||||||
|
grep -q 'sops:' "${path}" && grep -q 'ENC\[' "${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_unstaged_changes() {
|
||||||
|
local path="$1"
|
||||||
|
if git ls-files --error-unmatch -- "${path}" >/dev/null 2>&1; then
|
||||||
|
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --cached --name-only -- "${path}" | grep -Fxq "${path}"; then
|
||||||
|
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user