I use Authentik for SSO in my homelab and External-Secrets for 99% of my secrets so that I do not have to store them encrypted in the repo. It also makes it easier to rotate credentials etc. One thing that was a little irritating is that when I created a new application in Authentik I had to copy the client-id and client-secret to my external-secret for the application, duplicating the data. But no more!

External-Secrets has a webhook provider that is compatible with Authentik! I’ll outline the steps to integrate it real quick.

Authentik API Token

Go to Directory » Tokens and App passwords and click Create.

  • Identifier: external-secrets
  • User: <Some Admin User>
  • Intent: API Token
  • Description: Token for accessing app credentials via external-secrets
  • Expiring: true (or false if you are less risk averse)
  • Expires On: <Whatever date works for your threat model>

Now copy the token.

Creating the Webhook Provider

Create a secret with the token from the previous step. I’m putting everything in the external-secrets namespace. Since this is bootstrapping the External-Secrets system I would use SOPS to encrypt this at rest. You could probably use a regular SecretStore to pull it from some other external provider too.

apiVersion: v1
kind: Secret
metadata:
  name: authentik-secrets
  namespace: external-secrets
  labels:
    external-secrets.io/type: webhook
type: Opaque
stringData:
  AUTHENTIK_BOOTSTRAP_TOKEN: <TOKEN>

Create the External-Secrets webhook provider:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: authentik-oauth
  namespace: external-secrets
spec:
  provider:
    webhook:
      # This should be your in-cluster url for the authentik-server service
      url: "http://authentik-server.authentik.svc/api/v3/providers/oauth2/?name={{ .remoteRef.key }}"
      headers:
        Content-Type: application/json
        Authorization: Bearer {{ .auth.AUTHENTIK_BOOTSTRAP_TOKEN }}
      result:
        jsonPath: "$.results[0]"
      secrets:
        - name: auth
          secretRef:
            name: authentik-secrets
            namespace: external-secrets

Using the Provider

Now that the ClusterSecretStore is created, we can pull our client credentials like so:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: app1-oauth-secret
spec:
  secretStoreRef:
    kind: ClusterSecretStore
    name: authentik-oauth
  target:
    name: app1-oauth-secret
    template:
      data:
        client_id: "{{ .client_id }}"
        client_secret: "{{ .client_secret }}"
  dataFrom:
    - extract:
        # The name of the Provider in Authentik
        key: Provider for App1

Conclusion

This is greatly simplifying how I manage my service credentials. Hopefully it helps others too. Perhaps when Authentik 5675 gets implemented this won’t be necessary, but its definitely an improvement until then.