{"id":51858598,"url":"https://github.com/stuttgart-things/provider-kubeconfig","last_synced_at":"2026-07-24T03:31:09.526Z","repository":{"id":346624224,"uuid":"1190447213","full_name":"stuttgart-things/provider-kubeconfig","owner":"stuttgart-things","description":"manages remote Kubernetes cluster kubeconfigs","archived":false,"fork":false,"pushed_at":"2026-07-16T00:48:02.000Z","size":435,"stargazers_count":0,"open_issues_count":12,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T02:22:59.996Z","etag":null,"topics":["crossplane","crossplane-provider","kubeconfig","operator","sops"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stuttgart-things.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":"DCO","cla":null}},"created_at":"2026-03-24T09:41:37.000Z","updated_at":"2026-06-14T05:46:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/stuttgart-things/provider-kubeconfig","commit_stats":null,"previous_names":["stuttgart-things/xplane-provider-kubeconfig","stuttgart-things/provider-kubeconfig"],"tags_count":36,"template":false,"template_full_name":"crossplane/provider-template","purl":"pkg:github/stuttgart-things/provider-kubeconfig","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stuttgart-things%2Fprovider-kubeconfig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stuttgart-things%2Fprovider-kubeconfig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stuttgart-things%2Fprovider-kubeconfig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stuttgart-things%2Fprovider-kubeconfig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stuttgart-things","download_url":"https://codeload.github.com/stuttgart-things/provider-kubeconfig/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stuttgart-things%2Fprovider-kubeconfig/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35826032,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-24T02:00:07.870Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crossplane","crossplane-provider","kubeconfig","operator","sops"],"created_at":"2026-07-24T03:31:08.867Z","updated_at":"2026-07-24T03:31:09.495Z","avatar_url":"https://github.com/stuttgart-things.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# provider-kubeconfig\n\n[![CI - Build \u0026 Test](https://github.com/stuttgart-things/provider-kubeconfig/actions/workflows/build-test.yaml/badge.svg)](https://github.com/stuttgart-things/provider-kubeconfig/actions/workflows/build-test.yaml)\n[![Build, Push \u0026 Scan Container Image](https://github.com/stuttgart-things/provider-kubeconfig/actions/workflows/build-scan-image.yaml/badge.svg)](https://github.com/stuttgart-things/provider-kubeconfig/actions/workflows/build-scan-image.yaml)\n[![Latest Release](https://img.shields.io/github/v/release/stuttgart-things/provider-kubeconfig)](https://github.com/stuttgart-things/provider-kubeconfig/releases/latest)\n[![Go Report Card](https://goreportcard.com/badge/github.com/stuttgart-things/provider-kubeconfig)](https://goreportcard.com/report/github.com/stuttgart-things/provider-kubeconfig)\n[![Go Version](https://img.shields.io/github/go-mod/go-version/stuttgart-things/provider-kubeconfig)](go.mod)\n[![License](https://img.shields.io/github/license/stuttgart-things/provider-kubeconfig)](LICENSE)\n\n`provider-kubeconfig` is a [Crossplane](https://crossplane.io/) Provider that\nmanages remote Kubernetes cluster kubeconfigs. It reads kubeconfig files from\n**Git repositories** (SOPS-encrypted) or **HashiCorp Vault** (KVv2), and\nbootstraps Secrets and downstream ProviderConfigs for the remote clusters.\n\n## Features\n\n- **Dual source support** — read kubeconfigs from Git+SOPS or Vault KVv2\n- **Git-based kubeconfig management** — clones/pulls a Git repo and reads encrypted kubeconfig files\n- **SOPS/age decryption** — decrypts kubeconfigs encrypted with [SOPS](https://github.com/getsops/sops) using [age](https://age-encryption.org/) keys\n- **Vault KVv2 integration** — reads kubeconfigs from Vault with Kubernetes auth or AppRole auth\n- **Drift detection** — content hash comparison for Git, KVv2 version tracking for Vault\n- **Downstream ProviderConfigs** — automatically creates `provider-kubernetes` and `provider-helm` ProviderConfig/ClusterProviderConfig resources referencing the kubeconfig Secret\n- **ArgoCD cluster secrets** — optionally creates ArgoCD-compatible cluster secrets\n- **Cluster type detection** — auto-detects Kubernetes distribution (`kind`, `k3s`, `rke2`, `k8s`) from server version and node metadata\n- **Remote cluster status** — gathers metadata from the remote cluster (version, type, API endpoint, node count, CIDRs, internal network key) and exposes it in `status.atProvider`\n- **Stale git cache recovery** — automatically re-clones when pull fails with stale objects\n- **Structured logging** — logs key events (git clone, decryption, vault read, secret creation, downstream provisioning) for easier debugging\n- **RBAC self-bootstrap** — automatically creates/updates the ClusterRole and ClusterRoleBinding for downstream ProviderConfig management on startup\n\n## Custom Resource Types\n\n| Kind | Scope | Description |\n|------|-------|-------------|\n| `ProviderConfig` | Namespaced | Git/Vault + decryption settings (namespaced) |\n| `ClusterProviderConfig` | Cluster | Git/Vault + decryption settings (cluster-scoped) |\n| `RemoteCluster` | Cluster | Managed resource — reads kubeconfig, creates Secret + downstream ProviderConfigs |\n\n## Quick Start\n\n### 1. Install the Provider\n\n```yaml\napiVersion: pkg.crossplane.io/v1\nkind: Provider\nmetadata:\n  name: provider-kubeconfig\nspec:\n  package: ghcr.io/stuttgart-things/provider-kubeconfig-xpkg:v0.11.0\n```\n\n---\n\n## Source: Git + SOPS\n\n### Secrets\n\nCreate the age decryption key Secret (the key field must be named `key`):\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: age-key\n  namespace: crossplane-system\nstringData:\n  key: AGE-SECRET-KEY-1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n```\n\nFor private Git repos, create a token Secret (the field must be named `token`):\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: git-credentials\n  namespace: crossplane-system\nstringData:\n  token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n### ClusterProviderConfig (public repo)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: ClusterProviderConfig\nmetadata:\n  name: default\nspec:\n  git:\n    url: https://github.com/my-org/my-public-repo.git\n    branch: main\n    # revision: v1.2.3   # optional: pin to a commit SHA or tag for\n    #                    # deterministic, rollback-friendly reconciles.\n    #                    # When set, the branch tip is ignored.\n  decryption:\n    provider: sops\n    secretRef:\n      name: age-key\n      namespace: crossplane-system\n```\n\n### ClusterProviderConfig (private repo)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: ClusterProviderConfig\nmetadata:\n  name: my-private-repo\nspec:\n  git:\n    url: https://github.com/my-org/my-private-repo.git\n    branch: main\n    secretRef:\n      name: git-credentials\n      namespace: crossplane-system\n  decryption:\n    provider: sops\n    secretRef:\n      name: age-key-private\n      namespace: crossplane-system\n```\n\n### RemoteCluster (Git source)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: RemoteCluster\nmetadata:\n  name: my-cluster\nspec:\n  forProvider:\n    source:\n      path: clusters/my-cluster/kubeconfig.enc.yaml\n    secretNamespace: crossplane-system\n    providerConfigs:\n      - name: my-cluster-kubernetes\n        type: provider-kubernetes\n        apiVersions: [v2-cluster]\n      - name: my-cluster-helm\n        type: provider-helm\n        apiVersions: [v2-cluster]\n  providerConfigRef:\n    name: default\n    kind: ClusterProviderConfig\n```\n\n\u003e **Note:** Each repo/key combination needs its own ClusterProviderConfig. Multiple RemoteClusters can reference the same ClusterProviderConfig.\n\n---\n\n## Source: Vault KVv2\n\n### ClusterProviderConfig (Kubernetes auth)\n\nZero-config authentication — uses the provider pod's service account token:\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: ClusterProviderConfig\nmetadata:\n  name: vault-k8s\nspec:\n  vault:\n    address: https://vault.example.com\n    auth:\n      method: kubernetes\n      kubernetes:\n        role: provider-kubeconfig\n```\n\n### ClusterProviderConfig (AppRole auth)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: ClusterProviderConfig\nmetadata:\n  name: vault-approle\nspec:\n  vault:\n    address: https://vault.example.com\n    auth:\n      method: approle\n      appRole:\n        roleId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n        secretRef:\n          name: vault-approle-secret\n          namespace: crossplane-system\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: vault-approle-secret\n  namespace: crossplane-system\nstringData:\n  secret-id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### ClusterProviderConfig (Vault Enterprise namespace)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: ClusterProviderConfig\nmetadata:\n  name: vault-enterprise\nspec:\n  vault:\n    address: https://vault.example.com\n    namespace: my-team\n    mountPath: kv            # non-default KVv2 mount path\n    auth:\n      method: kubernetes\n      kubernetes:\n        role: provider-kubeconfig\n        mountPath: kubernetes  # auth mount path\n```\n\n### RemoteCluster (Vault source)\n\n```yaml\napiVersion: kubeconfig.stuttgart-things.com/v1alpha1\nkind: RemoteCluster\nmetadata:\n  name: my-cluster\nspec:\n  forProvider:\n    source:\n      type: vault\n      path: clusters/my-cluster       # KVv2 secret path\n      key: kubeconfig                  # key within the secret (default: kubeconfig)\n    secretNamespace: crossplane-system\n    providerConfigs:\n      - name: my-cluster-kubernetes\n        type: provider-kubernetes\n        apiVersions: [v2-cluster]\n      - name: my-cluster-helm\n        type: provider-helm\n        apiVersions: [v2-cluster]\n  providerConfigRef:\n    name: vault-k8s\n    kind: ClusterProviderConfig\n```\n\n### Storing a kubeconfig in Vault\n\n```shell\n# Write kubeconfig to Vault KVv2\nvault kv put secret/clusters/my-cluster kubeconfig=@kubeconfig.yaml\n\n# Verify\nvault kv get -field=kubeconfig secret/clusters/my-cluster\n```\n\n### Vault Drift Detection\n\nFor Vault sources, the provider tracks the KVv2 metadata version instead of content hashes. When you update the secret in Vault (creating a new version), the provider detects the version change and updates the Kubernetes Secret automatically.\n\n---\n\n## Verify\n\n```shell\n$ kubectl get remotecluster\nNAME         READY   SYNCED   CLUSTER      VERSION        TYPE   AGE\nmy-cluster   True    True     my-cluster   v1.35.1+k3s1   k3s    5m\n\n# Wide output shows NETWORK column\n$ kubectl get remotecluster -o wide\nNAME         READY   SYNCED   CLUSTER      VERSION        TYPE   NETWORK      AGE\nmy-cluster   True    True     my-cluster   v1.35.1+k3s1   k3s    10.31.102    5m\n```\n\n## Use the Kubeconfig\n\nExtract the decrypted kubeconfig to your local machine:\n\n```shell\nkubectl get secret kubeconfig-my-cluster \\\n  -n crossplane-system -o jsonpath='{.data.kubeconfig}' | base64 -d \u003e ~/.kube/my-cluster\n\nkubectl --kubeconfig ~/.kube/my-cluster get nodes\n```\n\n## API Version Labels\n\nThe `apiVersions` field on `providerConfigs` controls which downstream ProviderConfig types are created:\n\n| Label | API Group | Kind | Scope |\n|-------|-----------|------|-------|\n| `v1` | `*.crossplane.io` | `ProviderConfig` | Cluster |\n| `v2` | `*.m.crossplane.io` | `ProviderConfig` | Namespaced |\n| `v2-cluster` | `*.m.crossplane.io` | `ClusterProviderConfig` | Cluster |\n\nUse `v2-cluster` for Crossplane v2+ setups.\n\n## Cluster Type Detection\n\nThe provider auto-detects the Kubernetes distribution and writes it to `status.atProvider.clusterType`:\n\n| Distribution | Detection Method |\n|-------------|-----------------|\n| `k3s` | Server version contains `+k3s` |\n| `rke2` | Server version contains `+rke2` |\n| `kind` | Node name ends with `-control-plane` or `-worker`, and providerID is empty or starts with `kind://` |\n| `k8s` | Default fallback |\n\n## Encrypting a Kubeconfig with SOPS/age\n\n```shell\n# Generate an age key pair\nage-keygen -o age.key\n# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# Encrypt the kubeconfig\nsops encrypt --age age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\n  kubeconfig.yaml \u003e kubeconfig.enc.yaml\n\n# Store the secret key in Kubernetes (key field must be named \"key\")\nkubectl create secret generic age-key \\\n  --namespace crossplane-system \\\n  --from-literal=key=\"$(cat age.key | grep AGE-SECRET-KEY)\"\n```\n\n## RBAC\n\nThe provider bootstraps its own RBAC on startup to manage downstream ProviderConfigs. It creates:\n\n- **ClusterRole** `provider-kubeconfig-downstream` — permissions for `providerconfigs` and `clusterproviderconfigs` in `kubernetes.m.crossplane.io`, `helm.m.crossplane.io`, and the legacy `*.crossplane.io` APIs\n- **ClusterRoleBinding** `provider-kubeconfig-downstream` — binds to the provider's service account (auto-detected from the pod)\n\nThis is automatic — no manual RBAC setup required. On provider upgrades (new pod/SA name), the bootstrap appends the new SA to the binding.\n\n## Git Cache\n\nGit sources are cloned into a per-repo cache directory. The cache root is created with `0700` permissions so cached repo contents (kubeconfigs, `.git`) are not readable by other users sharing the pod. Least-recently-used directories are evicted once the entry count exceeds a cap, bounding disk usage on long-running pods.\n\n| Env var | Default | Description |\n|---------|---------|-------------|\n| `PROVIDER_KUBECONFIG_CACHE_DIR` | `$XDG_CACHE_HOME/provider-kubeconfig` (else `$TMPDIR/provider-kubeconfig`) | Cache root. Point at a dedicated writable volume (e.g. an `emptyDir`) to keep clones off shared `/tmp`. |\n| `PROVIDER_KUBECONFIG_CACHE_MAX_ENTRIES` | `32` | Max cached repo directories retained before LRU eviction. |\n\n## Observability\n\n### Metrics\n\nCustom Prometheus metrics are exposed on the manager's existing `/metrics` endpoint alongside the standard controller-runtime and crossplane metrics:\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `provider_kubeconfig_git_fetch_duration_seconds` | histogram | `repo`, `branch`, `operation`, `result` | Git clone/pull/revision latency. `operation` ∈ `clone\\|pull\\|revision`. |\n| `provider_kubeconfig_git_cache_total` | counter | `repo`, `branch`, `operation` | Git source operations, distinguishing fresh clone from cache-hit pull. |\n| `provider_kubeconfig_sops_decrypt_duration_seconds` | histogram | `format`, `result` | SOPS decrypt latency. |\n| `provider_kubeconfig_reconcile_errors_total` | counter | `stage` | Reconcile errors by stage (`git\\|decrypt\\|secret\\|downstream`). |\n\n### Tracing\n\nThe reconcile hot path emits OpenTelemetry spans (`git.EnsureCloned`, `git.ReadFile`, `sops.Decrypt`) so traces show which phase dominates. Tracing is **off by default** and activates when a standard OTLP endpoint is configured — e.g. set `OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317`. Standard `OTEL_*` env vars (headers, TLS, sampling) are honored.\n\n## Building\n\n### Prerequisites\n\n- Go 1.23+\n- Docker\n- Make\n\n### Build the Provider\n\n```shell\n# Initialize the build submodule (first time only)\nmake submodules\n\n# Generate CRDs, deepcopy, and run linters\nmake reviewable\n\n# Build the provider binary and Docker image\nmake build\n```\n\n### Local Development\n\n```shell\n# Create a kind cluster, install CRDs, and start the provider\nmake dev\n\n# Clean up\nmake dev-clean\n```\n\n### Running Tests\n\n```shell\ngo test ./internal/... -v -count=1\n```\n\n## Project Structure\n\n```\napis/\n  v1alpha1/                  # ProviderConfig, ClusterProviderConfig and usage types\n  kubeconfig/\n    v1alpha1/                # RemoteCluster managed resource type\ninternal/\n  cluster/                   # Remote cluster info gathering (version, type, nodes, CIDRs)\n  controller/\n    config/                  # ProviderConfig controller\n    remotecluster/           # RemoteCluster reconciler\n    kubeconfig.go            # Controller registration (SetupGated)\n  decrypt/                   # SOPS/age decryption\n  git/                       # Git clone/pull with caching and stale recovery\n  rbac/                      # RBAC self-bootstrap for downstream access\n  vault/                     # Vault KVv2 client with Kubernetes/AppRole auth\npackage/\n  crds/                      # Generated CRDs\n  crossplane.yaml            # Crossplane package metadata\n```\n\n## Provider Flags\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--debug` / `-d` | `false` | Enable debug logging (shows V(1) verbose logs) |\n| `--leader-election` / `-l` | `false` | Enable leader election for HA |\n| `--poll` | `1m` | How often to check each resource for drift |\n| `--sync` / `-s` | `1h` | Controller manager sync period |\n| `--max-reconcile-rate` | `10` | Max reconciliations per second |\n\n## Links\n\n- [Crossplane Provider Development Guide](https://github.com/crossplane/crossplane/blob/master/contributing/guide-provider-development.md)\n- [SOPS](https://github.com/getsops/sops)\n- [age encryption](https://age-encryption.org/)\n- [HashiCorp Vault](https://www.vaultproject.io/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstuttgart-things%2Fprovider-kubeconfig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstuttgart-things%2Fprovider-kubeconfig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstuttgart-things%2Fprovider-kubeconfig/lists"}