{"id":36465700,"url":"https://github.com/markhork8s/markhor","last_synced_at":"2026-01-12T00:02:42.358Z","repository":{"id":241437971,"uuid":"805720402","full_name":"markhork8s/markhor","owner":"markhork8s","description":"Bringing the security of SOPS to Kubernetes Secrets","archived":false,"fork":false,"pushed_at":"2025-08-27T12:30:01.000Z","size":660,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-27T21:38:47.550Z","etag":null,"topics":["kubernetes","operator","sops","sops-operator"],"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/markhork8s.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-05-25T09:19:44.000Z","updated_at":"2025-08-27T12:27:47.000Z","dependencies_parsed_at":"2025-06-24T16:26:19.040Z","dependency_job_id":"9f851670-6bc4-43e9-96a1-75ba68d6d874","html_url":"https://github.com/markhork8s/markhor","commit_stats":null,"previous_names":["markhork8s/markhor"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/markhork8s/markhor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markhork8s%2Fmarkhor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markhork8s%2Fmarkhor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markhork8s%2Fmarkhor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markhork8s%2Fmarkhor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markhork8s","download_url":"https://codeload.github.com/markhork8s/markhor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markhork8s%2Fmarkhor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28328695,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T22:11:01.104Z","status":"ssl_error","status_checked_at":"2026-01-11T22:10:58.990Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["kubernetes","operator","sops","sops-operator"],"created_at":"2026-01-12T00:02:42.279Z","updated_at":"2026-01-12T00:02:42.345Z","avatar_url":"https://github.com/markhork8s.png","language":"Go","funding_links":[],"categories":["Secret Management"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cpicture height=\"200px\"\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\".github/images/logo/dark.svg\"\u003e\n    \u003csource media=\"(prefers-color-scheme: light)\" srcset=\".github/images/logo/classic.svg\"\u003e\n    \u003cimg height=\"200px\" alt=\"The logo of Markhor\" src=\".github/images/logo/classic.png\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://go.dev\" \u003e\n    \u003cimg src=\"https://img.shields.io/badge/written%20in-go-blue?logo=go\" alt=\"Language\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/markhork8s/markhor/blob/main/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/license-Apache%202.0-green\" alt=\"License\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://deepwiki.com/markhork8s/markhor\"\u003e\n    \u003cimg src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/markhork8s/markhor\"\u003e\n    \u003cimg src=\"https://goreportcard.com/badge/github.com/markhork8s/markhor\" alt=\"Go report card\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/markhork8s/markhor/actions\"\u003e\n    \u003cimg src=\"https://github.com/markhork8s/markhor/actions/workflows/check_pr.yaml/badge.svg\" alt=\"Github CI status\"\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n# Markhor 🐐\n\n_\"Bringing the security of SOPS to Kubernetes Secrets\"_\n\nMarkhor is a [Kubernetes operator](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) meant to let your devs deploy [SOPS](https://github.com/getsops/sops)-encrypted Secrets inside your cluster. Lightweight, flexible and reliable.\n\n\u003e ️ℹ️ If you prefer to watch a video instead of reading, we got you covered: https://youtu.be/GMt8nquAGdw 👈\n\n---\n\n**Jump to section**:\n[Intro](#intro) -\n[Installation](#installation) -\n[Configuring](#configuring) -\n[MarkhorSecret Manifests](#markhorsecret-manifests) -\n[Directory Structure](#directory-structure) -\n[Code of Conduct](#code-of-conduct) -\n[Contributing](#contributing) -\n[Versioning](#versioning) -\n[Alternatives](#alternatives) -\n[Development Roadmap](#development-roadmap)\n\n---\n\n# Intro\n\n## What is this project?\n\nThis project aims to help developers managing their kubernetes Secrets. Specifically, it wants to facilitate the use of SOPS. If you do not know what SOPS is, check out [their repo](https://github.com/getsops/sops) first. We'll be here waiting 🍵\n\n## Why should I use it?\n\nWith respect to the [alternatives](#alternatives), we are the only one that lets you:\n\n- use SOPS to encrypt the secrets\n- will work independently of the other tools you use (e.g., you do not need to have Helm or FluxCD)\n- ensures cryptographically the integrity of the secret\n- is lightweight\n- adopts a 'set-it-and-forget-it' approach\n\n## Quickstart (TL;DR)\n\n- Setup:\n\n  1. Generate a public/private key pair\n\n     e.g., `age-keygen -o my_private.key`\n\n  1. Deploy Markhor on your cluster (see [installation](#installation)). Give it -and only to it- the private key.\n\n- Usage:\n\n  1. In your development environment, create a manifest of type `MarkhorSecret` -or use our script (available both in [bash](https://github.com/markhork8s/markhor/blob/main/utility_scripts/secret_to_markhorsecret.sh) and [python](https://github.com/markhork8s/markhor/blob/main/utility_scripts/secret_to_markhorsecret.py)) to convert an existing Secret automatically-.\n  1. **Encrypt** the manifest using SOPS and the public key.\n     \u003cdetails\u003e\n       \u003csummary\u003e\n       Example command to encrypt a MarkhorSecret manifest using SOPS\n       \u003c/summary\u003e\n\n     `sops encrypt --age \u003cthe_public_key\u003e  --encrypted-regex '^(data|stringData)$' ms.yaml \u003e ms.yaml`\n     \u003c/details\u003e\n\n  1. Use whichever magic you want to get the `MarkhorSecret` manifest to the cluster and kubectl apply it.\n\n     For example, you may commit the `MarkhorSecret` to git, push to a public/private repo on GitHub and have an instance of ArgoCD from your cluster pull the new manifest and apply it, in GitOps style.\n\n  1. Your work is done. Markhor will now automatically **decrypt** the manifest and create the corresponding `Secret` resource in the cluster. If the `MarkhorSecret` can't be decrypted, you will get an error when you kubectl apply it (if the admission hook is running. Else, you simply will not find the Secret and see the error in the Markhor pod's logs).\n\n  1. If you modify the `MarkhorSecret`, you guessed it, the `Secret` will be updated too 🔮 (provided its decryption goes well)\n\n![Illustration of the workflow when using Markhor to manage Kubernetes Secrets](.github/images/readme/explanation.svg)\n\n# Installation\n\nThis section covers how to install Markhor in your Kubernetes cluster AND how to start using it in a project as a developer.\n\nAs of now, there is no Kustomization/Helm chart available (see the [roadmap](#development-roadmap) section), but the steps are quite easy to follow.\n\nAs for the **resource requirements**, Markhor runs in a single pod which consumes around 30MB of RAM at idle. The container image weighs approximately 70MB.\n\n- For the cluster administrators (one time only):\n\n  1. On a machine with kubectl access to the cluster and these programs installed:\n\n     - kubectl\n     - git\n     - openssl\n     - age (https://github.com/FiloSottile/age)\n\n  1. Create a temporary directory and enter it by running `A=$(mktemp -d); cd $A`.  \n     This step ensures that the private key will not be stored on disk now. But if you did not [configure K8s to store its Secrets encrypted at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/), it's of little use since the final `markhor-age-secret` containing it swill be saved on disk in clear by K8s.\n\n  1. Generate a new key pair in age by running `age-keygen` (or use one you already have). Take note of the public and private key.\n\n  1. Clone this repo by running `git clone --depth 1 https://github.com/markhork8s/markhor`\n\n  1. Change directory into 'manifests' `cd ./markhor/manifests`\n\n  1. Edit the file `private_key_secret.yaml` by adding your private key in the field `age_keys.txt`\n\n  1. IF you do NOT want to use the validation admission controller (see [admission controller](#admission-controller-and-tls) section):\n\n     - Edit the `configmap.yaml` file to ensure that in `tls` the key `enabled` is set to false (if absent, the default value is false)\n\n     - Edit the `deployment.yaml`:\n\n       - In the `livenessProbe` and `readinessProbe` sections, remove `scheme` and the `httpHeaders` fields\n       - Remove the volume mount named `markhor-tls`\n       - Remove the volume named `markhor-tls`\n\n     - Edit the `apply.sh` script to remove the section specific to the admission controller (see comment)\n\n  1. Edit the `configmap.yaml` file to pass the desired options to the Markhor container (see section [configuring](#configuring)).\n\n  1. Run the script `./apply.sh`\n\n  1. Remove the temporary directory you created `cd ~; rm -r $A`\n\n  1. Check that the markhor pod is up and running `kubectl get pods -n markhor --watch`\n\n  1. Distribute the public key to the users\n\n- For the K8s cluster users (for each project):\n\n  1.  In the project's diretory, create a `.sops.yaml` file with settings for SOPS (see [their page](https://github.com/getsops/sops) for details), similar to the following:\n\n      ```yaml\n      keys:\n        # The public age key\n        - \u0026mykey age1apq7ck...8pe8y\n      creation_rules:\n        # Telling sops to manage all the yaml/yml files with a name ending with '_secret'\n        - path_regex: \".*_secret.ya?ml\"\n          key_groups:\n            - age:\n                - *mykey\n          # Telling sops to encrypt only the \"data\" and \"stringData\" fields\n          encrypted_regex: ^(data|stringData)$\n      ```\n\n  1.  Create a `MarkhorSecret` manifest and encrypt it with SOPS (e.g., by running `sops new_secret.yaml`).\n      Here is an example of a file you may create.\n\n      Note that, apart from the `markhorParams` field, it is exactly the manifest of the K8s Secret you'd create normally -and see section [MarkhorSecret files](#markhorsecret-manifests) for details on the syntax of a `MarkhorSecret` manifest-.\n\n      ```yaml\n      apiVersion: markhork8s.github.io/v1\n      kind: MarkhorSecret\n      metadata:\n        name: my-new-awesome-app-secret\n        namespace: my-new-awesome-app\n      markhorParams:\n        order:\n          - apiVersion\n          - kind\n          - metadata/name\n          - metadata/namespace\n          - markhorParams/order\n          - data/sessionSecret\n      data:\n        sessionSecret: aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUT8=\n      ```\n\n      \u003cdetails\u003e\n        \u003csummary\u003e\n        Once encrypted with SOPS, it looks like this\n        \u003c/summary\u003e\n\n      ```yaml\n      apiVersion: markhork8s.github.io/v1\n      kind: MarkhorSecret\n      metadata:\n        name: my-new-awesome-app-secret\n        namespace: my-new-awesome-app\n      markhorParams:\n        order:\n          - apiVersion\n          - kind\n          - metadata/name\n          - metadata/namespace\n          - markhorParams/order\n          - data/sessionSecret\n      data:\n        sessionSecret: ENC[AES256_GCM,data:ZkWkp381nnkvozuBqHUMbyRhZelfJk69J8m/kxOuXexJKVBpbsGOBQ==,iv:YMaJm1YUgTMbV7IPccOQ5WjFPBxv1fEhibVglu426bY=,tag:EK3q6dg0iDNe7bP6fjCzIA==,type:str]\n      sops:\n        kms: []\n        gcp_kms: []\n        azure_kv: []\n        hc_vault: []\n        age:\n          - recipient: age1apq7ck5adq6dkd0c242phl42fsurvpxvt9pwk0qg7ahdex7fqppqj8pe8y\n            enc: |\n              -----BEGIN AGE ENCRYPTED FILE-----\n              YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLOWtsNnBXNWdYL05qVXVO\n              RGJCc0VlaDVTZ0JuQyswRCtLSHJQcUZFSGdBCktMUHgzYlppSGJEL1lyeTNtY2tv\n              VExzMzBvaUNIeExjUFBwNTQrQ1VuNXMKLS0tIEVRZWd6MjhxVTNQRmhxeVVPZkhl\n              THUzS3BRQWR4OEJiVEN0ODRzTXY5ejAKkflmiuAW1b6/63tFcI49nWjIzi1RGHKA\n              NAyMjW7F2pO884j2rSCHIK1+SKItlS72QhZjC8cK4Xxm0yFPGOnjCQ==\n              -----END AGE ENCRYPTED FILE-----\n        lastmodified: \"2024-05-04T18:55:50Z\"\n        mac: ENC[AES256_GCM,data:CASz0h/fcr1TCVX334YCjI4w8/H1aDjL9qhosYlGVyalJc6XdjvfbM/lCl2y6bsZ75+O5Yh8hqVyjciJvVdIQ5UUKIJCNZkMuS9iESRFuP+Mt2qsK595In0kBoG4ZTD3YOINcNwnPFTfXzQB6Ffp8wSSTzEjIzW1uAS7U32plcM=,iv:HDxJA01ufCP4k05GZgQAQbpDlZ4xc2A4q4Lq710YFV4=,tag:SMHlmW4u400YjnnxyYf+1g==,type:str]\n        pgp: []\n        encrypted_regex: ^(data|stringData)$\n        version: 3.8.1\n      ```\n\n      \u003c/details\u003e\n\n  1.  Now you can commit/push this file wherever you want. Once it reaches the cluster, the Markhor pod running inside it will take care of decrypting it and creating the corresponding secret.\n\n# Configuring\n\nThis section covers the configuration options available for the Markhor operator (the component which runs inside the K8s cluster).\n\nIt is configured exclusively through one yaml file. The default location for this file is `/etc/markhor/config.yaml`. You may specify an alternative path for such file using the `--config` argument when invoking the program.\n\nA complete JSON-schema is available in [./docs/json_schema/markhor.json](https://github.com/markhork8s/markhor/blob/main/docs/json_schema/markhor.json).  \nIf you are a VSCode user, you can import it to enable automatic linting and suggestions by adding this line on the top of your `config.yaml` file -provided you have the [redhat.vscode-yaml](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) extension installed-: `# yaml-language-server: $schema=https://raw.githubusercontent.com/markhork8s/markhor/main/docs/json_schema/markhor.json`.\n\nHere is a table of the available options. The dot indicates nesting, so `a.b` means that there is a field named `b` inside a parent field named `a`.\n\n\u003c!-- This table was generated automatically by ./utility_scripts/json_schema_to_md_table.py using the file ./docs/json_schema/markhor.json --\u003e\n\n| Property name                                   | Type          | Default value                  | Description                                                                                                                                                                                                                                                                                                                            |\n| ----------------------------------------------- | ------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| kubernetes.kubeconfigPath                       | string        | \"\"                             | Path to the file containing the information necessary to connect with the kubernetes cluster (More info at https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/). If Markhor will run inside the cluster, leave it unspecified so it uses the default value of \"\".                                    |\n| kubernetes.clusterTimeoutSeconds                | integer       | 10                             | The amount of time, in seconds, that Markhor should wait when connecting to the cluster before timing out.                                                                                                                                                                                                                             |\n| healthcheck.port                                | integer       | 8000                           | The port number where the healthcheck endpoint should listen on                                                                                                                                                                                                                                                                        |\n| healthcheck.enabled                             | boolean       | True                           | Wether to enable the healthcheck or not                                                                                                                                                                                                                                                                                                |\n| admissionController.port                        | integer       | 443                            | The port number where the admission controller should listen on                                                                                                                                                                                                                                                                        |\n| admissionController.enabled                     | boolean       | True                           | Wether to enable the admission controller or not                                                                                                                                                                                                                                                                                       |\n| tls.enabled                                     | boolean       | False                          | If true, the healthcheck and validation admission controller will run on HTTPS. If false, they will run on HTTP. Default: `false`                                                                                                                                                                                                      |\n| tls.certPath                                    | string        | \"/etc/markhor/tls/tls.crt\"     | Path (seen from inside the Markhor pod) where the TLS certificate file is stored.                                                                                                                                                                                                                                                      |\n| tls.keyPath                                     | string        | \"/etc/markhor/tls/tls.key\"     | Path (seen from inside the Markhor pod) where the TLS private key file is stored.                                                                                                                                                                                                                                                      |\n| logging.level                                   | string        | \"info\"                         | The verbosity of the logs (see https://github.com/sirupsen/logrus?tab=readme-ov-file#level-logging)                                                                                                                                                                                                                                    |\n| logging.style                                   | string        | \"text\"                         | The format of the logs (as provided by slog)                                                                                                                                                                                                                                                                                           |\n| logging.logToStdout                             | boolean       | True                           | Wether to log the output of this program to stdout                                                                                                                                                                                                                                                                                     |\n| logging.additionalLogFiles                      | array[string] | []                             | List of files (besides stdout) where the logs of this process should be written to                                                                                                                                                                                                                                                     |\n| behavior.fieldmanager.name                      | string        | \"markhork8s.github.io\"         | Name of the field manager that Markhor will give to kubernetes                                                                                                                                                                                                                                                                         |\n| behavior.fieldmanager.forceUpdates              | boolean       | False                          | If this is inactive -default-, Markhor will not modify secrets which have another fieldmanager -e.g., the ones created by the admins or other apps-. On the contrary, if this is active, Markhor will take over the field manager -overriding existing secrets-. In any case, when the field manager mismatches, a warning is printed. |\n| behavior.namespaces                             | array[string] | []                             | List of all the namespaces where Markhor is allowed to operate in. An empty list -the default- signifies that Markhor will operate on all namespaces                                                                                                                                                                                   |\n| behavior.excludedNamespaces                     | array[string] | []                             | List of all the namespaces where Markhor is forbidden to operate in. This has higher priority than the \"namespaces\" field.                                                                                                                                                                                                             |\n| markorSecrets.hierarchySeparator.default        | string        | \"/\"                            | Which character (or string) is used as a marker for indentation in markhorParams\u003eorder. The dafault value is \"/\", meaning that the string \"a/b\" indicates a property that in JSON would be \"a\":{\"b\":\"some-value\"} while \"a.b\" indicates a property that in JSON would be \"a.b\":\"some-value\"                                            |\n| markorSecrets.hierarchySeparator.allowOverride  | boolean       | True                           | Wether a MarkhorSecret manifest can override the value of the default hierarchy separator defined in this configuration file.                                                                                                                                                                                                          |\n| markorSecrets.hierarchySeparator.warnOnOverride | boolean       | False                          | Wether to print a warning when a MarkhorSecret manifest overrides the value of the default hierarchy separator defined in this configuration file. If false, it prints a debug message. Defaults to false.                                                                                                                             |\n| markorSecrets.managedLabel.default              | string        | \"app.kubernetes.io/managed-by\" | The name of the label that Markhor adds to the Secrets it manages. https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels                                                                                                                                                                                     |\n| markorSecrets.managedLabel.allowOverride        | boolean       | False                          | If false -default-, a MarkhorSecret manifest cannot override the value of the custom label defined in this configuration file -and a warning is printed if it tries to do so-.                                                                                                                                                         |\n| markorSecrets.managedLabel.warnOnOverride       | boolean       | True                           | Wether to print a warning when a MarkhorSecret manifest overrides the value of the custom label defined in this configuration file. Defaults to true                                                                                                                                                                                   |\n\n\u003c!-- This table was generated automatically by ./utility_scripts/json_schema_to_md_table.py using the file ./docs/json_schema/markhor.json --\u003e\n\nAll the configuration relative to SOPS is currently done by using environment variables and mounting volumes in the pod (e.g., setting `SOPS_AGE_KEY_FILE` and mounting that file). See the [SOPS repository](https://github.com/getsops/sops) for more info.\n\n## Admission Controller and TLS\n\nAn optional component of the Markhor program is the admission controller. The admission controller provides a validation webhook which lets Kubernetes check if a `MarkhorSecret` is valid when kubectl applying it.\nWe encourage you to enable this feature, especially if you use CD tools since it lets the CD tool know if the `MarkhorSecret` failed to decrypt.\n\nKubernetes admission hooks, however, require to be exposed over HTTPS ([reference docs](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/)). If you perform the TLS termination outside of the markhor container, just ensure `tls.enabled` is set to false in the Markhor config (the default). Otherwise, you need to provide Markhor with the TLS certificate and key.\n\nTo generate the TLS certificate and key, you may use the script [./manifests/gen_hook_tls_secret.sh](https://github.com/markhork8s/markhor/blob/main/manifests/gen_hook_tls_secret.sh).\nThen you need to ensure they are mounted in the pod, that there is a service where K8s can reach it, and that the healthchecks are configured to use HTTPS -as of now, this is the configuration in [./manifests](https://github.com/markhork8s/markhor/blob/main/manifests)-.\n\n# MarkhorSecret Manifests\n\nThis section explains how a `MarkhorSecret` manifest is structured and what the requirements for making it decypherable are.\n\n**Pro tip 💎**: You can convert instantly existing Secrets to MarkhorSecrets by using the script in `./utility_scripts/` (available both in [bash](https://github.com/markhork8s/markhor/blob/main/utility_scripts/secret_to_markhorsecret.sh) and [python](https://github.com/markhork8s/markhor/blob/main/utility_scripts/secret_to_markhorsecret.py)).\n\n## File structure\n\nA MarkhorSecret manifest aims to look exactly the same as a Kubernetes Secret, with one exception: the field `markhorParams` -and the `kind` and `apiVersion` fields-.\n\n```yaml\napiVersion: markhork8s.github.io/v1\nkind: MarkhorSecret\nmetadata:\n  name: my-new-awesome-app-secret\n  namespace: my-new-awesome-app\nmarkhorParams:\n  order:\n    - apiVersion\n    - kind\n    - metadata/name\n    - metadata/namespace\n    - markhorParams/order\n    - data/sessionSecret\ndata:\n  sessionSecret: aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUT8=\n```\n\nWhy do we need that `markhorParams` field? Glad you asked. The core issue is the following: in both YAML and JSON _arrays are ordered, object properties are not_ (link to the [YAML](https://yaml.org/spec/1.2.2/#sequence) and [JSON](https://www.json.org/json-en.html) specifications). When dealing with a file encrypted by SOPS, if the properties are re-arranged, the Message Authentication Code (MAC) -which prevents alterations of the file- changes and the decryption process fails.\n\nFor Markhor, this issue manifests itself when we give an encrypted `MarkhorSecret` manifest to Kubernetes, which reorders the fields in alphabetical order.\n\nTo cope with this, we can either write the manifests with an order that gets preserved in transit (but anything which touches the manifest may change it) OR find a way to let the Markhor process running in the pod know how to reorder the file before attempting to decrypt it. The second option is the only feasible one -because the first may break anytime an implementation, say the one of K8s, changes-.\n\nTherefore, I considered the following options:\n\n1. Define a fixed ordering ourselves. E.g., \"MakrhorSecrets shall always be written in alphabetical order\". A bit stringent, but would work since, even if the file is modified in transit, the markhor program can reorder it alphabetically before decrypting it.\n1. Define ordering \"templates\" that specify conventional orderings (e.g., `mytemplate1: [apiVersion,kind,metadata,data]`). Then you either specify the name of the ordering to use in the `MarkhorSecret` manifest or the Markhor pod tries a set of approved orderings. I am not particularly fond of tihs one because it would introduce the need to manage and sync these two entities (`MarkhorSecret`s and the ordering templates).\n1. You include in each `MarkhorSecret` the order of the fields in a way that it does not get altered by intermediaries like K8s.\n\nI decided to go with the first and last option: Each `MarkhorSecret` specifies the order of its fields, and alphabetic ordering is used as a fallback if such ordering is not present. This way, final users can choose the method they prefer.\n\nIf the ordering is specified inside the `MarkhorSecret`, it is included in `markhorParams.order` as an array -which is an ordered data structure both for YAML and JSON- of strings. The array contains an item for each field of the manifest which is not an object (including the `markhorParams` themselves). The order of the elements in the array is the same as the order of the corresponding fields in the manifest. The character `/` denotes nested properties (`a.b`=`{\"a.b\":\"some_value\"}`, `a/b`=`{\"a\":{\"b\":\"some_value\"}}`). If necessary, this character can be changed with any arbitrary string using the `hierarchySeparator` property.\n\nWith reference to the previous example, we could replace the separator `/` with `.` by writing:\n\n```yaml\nmarkhorParams:\n  hierarchySeparator: \".\"\n  order:\n    - apiVersion\n    - kind\n    - metadata.name\n    - metadata.namespace\n    - markhorParams.hierarchySeparator\n    - markhorParams.order\n    - data.sessionSecret\n```\n\nThe markhorParams field may also have an additional property: `managedLabel`. It overrides the default managed-by label for the current `MarkhorSecret`. See `markorSecrets.managedLabel` in the [configuring section](#configuring) for more info on this.\n\n# Directory Structure\n\n```\nMarkhor\n├── docs                      Documentation + JSON schema 📚\n├── manifests                 K8s manifests 📄\n├── pkg                       Source code   🧬\n└── utility_scripts           Helpful miscellanea scripts 🛠\n```\n\nAll the code is in [./pkg](./pkg), except for `main.go`.\n\n# Code of Conduct\n\nThis project follows the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct.\nYou can find the details in [./CODE_OF_CONDUCT.md](https://github.com/markhork8s/markhor/blob/main/CODE_OF_CONDUCT.md)\n\n# Contributing\n\nIf you'd like to contribute, that is great!  \nYou can find the details in [./CONTRIBUTING.md](https://github.com/markhork8s/markhor/blob/main/CONTRIBUTING.md)\n\nIf it is one of your first times contributing to open source projects, I strongly sugget you to take 30 minutes to read https://opensource.guide/how-to-contribute/. It is a curated series of articles that makes you aware of many little details that make great differences for both you and the maintainers of this project.\n\n# Versioning:\n\nThis project follows [semantic versioning (v2.0)](https://semver.org).\n\n# Alternatives\n\nThis section lists the other projects I know of that aim to help managing Kubernetes Secrets more securely.  \nI include a comparison with Markhor. Choose the one you find works best for your use-case, I won't judge 😜.\n\nIf you are a maintainer of any of these projects and notice I glanced over something, let me know.\n\n## Helm Secrets:\n\n[Helm Secrets](https://github.com/jkroepke/helm-secrets), from what I saw, enables you to easily manage K8s secrets with SOPS, as long as your apps are packaged with Helm.\n\n## FluxCD:\n\nIf you use Flux for your CD, it has built-in support for SOPS ([link](https://fluxcd.io/flux/guides/mozilla-sops/)).\n\n## Sealed Secrets:\n\n[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) is a project from bitnami. From what I have seen it is very similar to Markhor, but does not use SOPS to do the encryption/decryption. Instead, it uses its own CLI utility called `kubeseal`.\n\n## SOPS Secrets Operator:\n\nFrom what I saw, the [SOPS Secrets Operator](https://github.com/isindir/sops-secrets-operator) provides essentially the same functionality of this project, but **alters the SOPS decryption function to skip the MAC check**. This means that the decryption is successful even if the encrypted file was altered, introducing a security concern.\n\n## ArgoCD:\n\nThe support for SOPS ArgoCD appears to only be available through plugins.\nLooking at these resources, the integrations seem a bit too hacky for me:\n\n- https://blog.pelo.tech/k-markhor-argocd-54becd3a1a34\n- https://community.ops.io/jilgue/secrets-in-argocd-with-sops-pa6\n- https://medium.com/@CoyoteLeo/security-upgrade-with-sops-5d4a1385c680\n- https://www.redhat.com/en/blog/a-guide-to-gitops-and-secret-management-with-argocd-operator-and-sops\n\n# Development Roadmap\n\nHere is a recap of where we are in the development of this project.\n\nFinished features:\n\n- Custom Resource Definition `MarkhorSecret` ✅\n- Kubernetes operator to convert `MarkhorSecret`s into `Secret`s ✅\n  - Create a `Secret` when a `MarkhorSecret` is created ✅\n  - Update a `Secret` when a `MarkhorSecret` is updated ✅\n  - Delete a `Secret` when a `MarkhorSecret` is deleted ✅\n- Healthcheck ✅\n- Logging with slog and different levels ✅\n- Testing ✅\n- Validation hook to let CD know of malformed `MarkhorSecret`s ✅\n- Honor all the values set in the config file ✅\n- Video tutorial ([link](https://youtu.be/GMt8nquAGdw)) ✅\n\nTODOs (help very much appreciated 🐜):\n\n- Badges\n  - https://www.bestpractices.dev/en\n  - https://goreportcard.com/\n- Streamlining the installation process with helm/kustomize\n- Horizontal scaling + high availability (redis/dragonflydb?)\n- Ensuring support for all the methods allowed by SOPS (see [./docs/methods.md](https://github.com/markhork8s/markhor/blob/main/docs/methods.md)):\n  - age ✅\n  - AWS KMS ✅\n  - GCP KMS ✅\n  - Azure Key Vault ✅\n  - pgp\n  - Hashicorp Vault\n- Metrics endpoint for prometheus _et similia_\n- Testing the K8s event handlers in the code using the mock cluster interface\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkhork8s%2Fmarkhor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkhork8s%2Fmarkhor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkhork8s%2Fmarkhor/lists"}