ansible / awx-operator

An Ansible AWX operator for Kubernetes built with Operator SDK and Ansible. 🤖
https://www.github.com/ansible/awx
Apache License 2.0
1.26k stars 632 forks source link

Allow extra_settings to accept valueFrom to inject secrets #1224

Open einermart opened 1 year ago

einermart commented 1 year ago

Please confirm the following

Feature Summary

I am trying to setup Keycloak authentication and I would like to inject the different ENVs needed from a secret. Currently extra_settings does not allow to inject envs from secrets. This would be a great feature to add.

Thank you

fosterseth commented 1 year ago

Hi, can you provide more information about what you are trying to accomplish? e.g. are you attempting to use the keycloak module within AWX?

more information about the specific ENVs would be helpful

einermart commented 1 year ago

Hello, I sure can.

I am trying to configure the following OIDC ENVs using extra_settings:

SOCIAL_AUTH_OIDC_KEY
SOCIAL_AUTH_OIDC_SECRET
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT

I want to be able to set those ENVs referencing an existing secret within my namespace.

  extra_settings:
    - name: SOCIAL_AUTH_OIDC_KEY
      valueFrom:
        secretKeyRef:
          name: awx-operator-keycloak-credentials
          key: client-id
    - name:  SOCIAL_AUTH_OIDC_SECRET
      valueFrom:
        secretKeyRef:
          name: awx-operator-keycloak-credentials
          key: client-secret
    - name: SOCIAL_AUTH_OIDC_OIDC_ENDPOINT
      valueFrom:
        secretKeyRef:
          name: awx-operator-keycloak-credentials
          key: realm_url

Using the method above fails due to valueFrom being an unrecognized field. I hope that provides a bit more information. Thank you

yuha0 commented 1 year ago

+1

Currently, LDAP seems to be the only authentication backend that's capable of loading sensitive information (i.e. LDAP bind password) from a k8s secret, thanks to load_ldap_password_secret.yml.

To enable a "social" auth provider (GitHub, AzureAD...etc), one has to initialize an AWX instance with a local admin, use the local admin account to login, and manually insert provider's secrets (OAuth secrets, SAML certificates...) on a webpage. I want to be able to control auth provider configuration in an infrastructure-as-code way.

antverpp commented 1 year ago

this should be very useful

NeilHanlon commented 1 year ago

Could someone point me to how I might begin to implement this feature? I'm happy to take a crack at it.

kurokobo commented 1 year ago

The keys and values that passed over extra_settings will be embedded to settings.pyas a part of Python code. Currently there is no handy way to pass values from Secrets to extra_settings.

As a workaround, this is a bit tricky way, but you can add environment variable from Secrets, and pass extra_settings with the code that reads values from environment variables.

---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
  name: awx
spec:
  ...

  task_extra_env: |
    - name: SOCIAL_AUTH_AZUREAD_OAUTH2_KEY
      valueFrom:
        secretKeyRef:
          name: awx-azuread-oauth2-secret
          key: key
    - name: SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET
      valueFrom:
        secretKeyRef:
          name: awx-azuread-oauth2-secret
          key: secret
  web_extra_env: |
    - name: SOCIAL_AUTH_AZUREAD_OAUTH2_KEY
      valueFrom:
        secretKeyRef:
          name: awx-azuread-oauth2-secret
          key: key
    - name: SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET
      valueFrom:
        secretKeyRef:
          name: awx-azuread-oauth2-secret
          key: secret

  extra_settings:
    - setting: SOCIAL_AUTH_AZUREAD_OAUTH2_KEY
      value: 'os.getenv("SOCIAL_AUTH_AZUREAD_OAUTH2_KEY")'
    - setting: SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET
      value: 'os.getenv("SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET")'

You can see the environment variables SOCIAL_AUTH_AZUREAD_OAUTH2_KEY and SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET have the actual values and the settings in settings.py are configured to read values from environment variables.

$ kubectl -n awx exec -it deployment/awx-web -c awx-web -- env | grep SOCIAL_AUTH_AZUREAD
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY=****************
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET=****************

$ kubectl -n awx exec -it deployment/awx-web -c awx-web -- cat /etc/tower/settings.py
import os
import socket
...
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = os.getenv("SOCIAL_AUTH_AZUREAD_OAUTH2_KEY")
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = os.getenv("SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET")
...
tylergmuir commented 11 months ago

@kurokobo Your workaround does work for string values but does not work for json values. It also appears that you cannot inject the python code for os.getenv("ENV_VARIABLE") in to a json value as the jinja2 templating it putting quotes around it causing it to be read as a string instead of being executed.

Any ideas on how to handle this? Or would that just require code changes to be made to support that functionality?

kurokobo commented 11 months ago

@tylergmuir Can you provide some example keys and values that you want to inject? In particular, how would you like to use Jinja2?

tylergmuir commented 10 months ago

@kurokobo The specific use-case I had was with the SOCIAL_AUTH_SAML_ENABLED_IDPS setting. It takes in a json value like this:

{
  "saml_ms_adfs": {
    "x509cert": "${SAML_IDP_SIGNING_CERT}",
    "attr_last_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
    "attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
    "attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "attr_username": "name_id",
    "attr_user_permanent_id": "name_id",
    "entity_id": "https://adfs.domain.com/adfs/services/trust",
    "url": "https://adfs.domain.com/adfs/ls"
  }
}

The only thing that I would require to be passed as a secret is the x509cert value, but it is workable if I had to pass the entire value as a secret. But if you try subbing "${SAML_IDP_SIGNING_CERT}" with os.getenv("SOME_ENV_VAR") it gets surrounded with quotes like it is a string. Alternatively, if you pass the entire value as a env var, it ends up with a parsing error as it is collected a one giant string value.

This could probably be solved by using the json python module, but without making upstream modification to include additional modules for parsing the settings file, I don't see a way to handle this currently.

kurokobo commented 10 months ago

@tylergmuir

It takes in a json value like this

Not JSON, but Dict in python. so you dont't need quoting os.getenv("SOME_ENV_VAR"). I can't fully test this since I don't have any labs for SAML auth, but I can deploy AWX without any errors with SAML configuration through extra_settings with the cert from secret.

I think the following two are important:

# Create a secret based on a literal string
# because it contains newlines and headers/footers when created based on a PEM file
$ kubectl -n awx create secret generic awx-saml-idp-signing-cert --from-literal=x509cert=MIIDZTCCAk2gAwIB................8AL9sqeB6xGPxz26
spec:
  ...
  # Define env SAML_IDP_SIGNING_CERT for task and web pod
  task_extra_env: |
    - name: SAML_IDP_SIGNING_CERT
      valueFrom:
        secretKeyRef:
          name: awx-saml-idp-signing-cert
          key: x509cert
  web_extra_env: |
    - name: SAML_IDP_SIGNING_CERT
      valueFrom:
        secretKeyRef:
          name: awx-saml-idp-signing-cert
          key: x509cert

  extra_settings:
    - setting: SOCIAL_AUTH_SAML_ENABLED_IDPS 
      # Define value as multiline strings with 2 spaces indentation (by "|2")
      # And pass the actual value with "6" spaces indentation
      # to add 4 leading spaces for each lines to keep indentation in ConfigMap correctly
      value: |2
            {
              "saml_ms_adfs": {
                "x509cert": os.getenv("SAML_IDP_SIGNING_CERT"),
                "attr_last_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
                "attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
                "attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
                "attr_username": "name_id",
                "attr_user_permanent_id": "name_id",
                "entity_id": "https://adfs.domain.com/adfs/services/trust",
                "url": "https://adfs.domain.com/adfs/ls"
              }
            }

Result:

image

If you want the contents of the secret to be the PEM file including newlines and headers and footers, you will need to format os.getenv with replace or similar:

                "x509cert": os.getenv("SAML_IDP_SIGNING_CERT").replace("\n", "").replace("-----BEGIN CERTIFICATE-----", "").replace("-----END CERTIFICATE----", ""),
tylergmuir commented 10 months ago

@kurokobo That worked perfectly! Since it doesn't seem like this issue has made any progress and is almost a year old, I think it would be good to open a PR with your examples to provide some documentation for others without having to read through all of the comments in the issue. I would be happy to write that up, but I don't want to take credit for your work, so if you would like to do it instead I am good with that too.

Thanks again!

kurokobo commented 10 months ago

@tylergmuir

I think it would be good to open a PR with your examples to provide some documentation for others without having to read through all of the comments in the issue. I would be happy to write that up, but I don't want to take credit for your work, so if you would like to do it instead I am good with that too.

Thanks for your concern, if you are interested in becoming a contributor, I would love to have you create a PR for the community. I will review it if needed 😃 I was thinking of adding it to the tips section of my guide, but if it is covered in the official documentation, so much the better.