Skip to content

Conversation

pasha-r
Copy link
Contributor

@pasha-r pasha-r commented Jan 17, 2020

We had a brief discussion about this issue here: #341

Description

  1. add client-secret-file option
  2. make ProviderData's ClientSecret a function
  3. if client-secret-file is set ClientSecret() reads the file an returns its context
  4. if client-secret-file is not set ClientSecret() behaves the usual way
  5. I replaced all p.ClientSecret reads with p.ClientSecret() call
  6. if client-secret-file is set and file read fails redeem call will return 500 error

Motivation and Context

Some OAuth2 identity management systems rotate client secrets and upload such secrets to running applications as a file in a mounted volume. I would like to be able to read client secret from an in-memory file every time I need to issue a redeem or redeem refresh request.

How Has This Been Tested?

I checked that all existing tests run correctly. I added tests for Options and DataProvider. And checked that Redeem fails if the client secret file is missing.

Checklist:

  • My change requires a change to the documentation or CHANGELOG.
  • I have updated the documentation/CHANGELOG accordingly.
  • I have created a feature (non-master) branch for my PR.

@pasha-r pasha-r requested a review from a team January 17, 2020 15:59
@pasha-r
Copy link
Contributor Author

pasha-r commented Jan 21, 2020

Do I need to create a feature branch for this PR?

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @pasha-r, thanks for implementing this!

I'm pretty happy with this in general, thought I did leave a couple of comments for potential improvements

@pasha-r
Copy link
Contributor Author

pasha-r commented Feb 3, 2020

Updated the naming: the method is GetClientSecret now and the field is ClientSecret

Comment on lines +230 to +235
if o.ClientSecret == "" && o.ClientSecretFile != "" {
_, err := ioutil.ReadFile(o.ClientSecretFile)
if err != nil {
msgs = append(msgs, "could not read client secret file: "+o.ClientSecretFile)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we read the file and do nothing with it? Just to check it's valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one nit, otherwise looking good

Please add a note to the changelog to explain the change and then we can get this merged

pasha-r and others added 3 commits February 7, 2020 15:08
@pasha-r
Copy link
Contributor Author

pasha-r commented Feb 7, 2020

CHANGELOG entry - done.
Thank you!

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@col-panic
Copy link

I don't understand the documentation If you set up your OAuth2 provider to rotate your client secret, you can use the client-secret-file option to reload the secret when it is updated.

Does this mean I have to manually call this on the oauth2-proxy executable? At the moment I start oauth2-proxy and the secret gets updated while the program is running - oauth2-proxy does not seem to pickup the change automatically?!

@tuunit
Copy link
Member

tuunit commented Jan 10, 2024

Hi @col-panic could you share your configuration?

@col-panic
Copy link

I have a docker compose configuration with the service like this

services:
  # https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview
  # https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider#keycloak-oidc-auth-provider
  # https://github.com/bitnami/containers/tree/main/bitnami/oauth2-proxy
  oauth2-proxy:
    image: bitnami/oauth2-proxy:7
    networks:
      - backend
    expose:
      - 4180
    volumes:
      - siteconfig:/mnt/siteconfig/
    depends_on:
      ee-config:
        condition: service_completed_successfully
      web:
        condition: service_started
    environment:
      - OAUTH2_PROXY_COOKIE_SECRET=CRtsmWgUeR5sFMVqk9SdyAySkfYvCMkIWSyKKtgXDYY=
      - OAUTH2_PROXY_PROVIDER=keycloak-oidc
      - OAUTH2_PROXY_CLIENT_ID=oauth2-proxy
      - OAUTH2_PROXY_CLIENT_SECRET_FILE=/mnt/siteconfig/oauth2-proxy.secret
      - OAUTH2_PROXY_REDIRECT_URL=https://$EE_HOSTNAME/oauth2/callback
      - OAUTH2_PROXY_OIDC_ISSUER_URL=https://$EE_HOSTNAME/keycloak/auth/realms/ElexisEnvironment
      - OAUTH2_PROXY_CODE_CHALLENGE_METHOD=S256
      - OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST=true
      - OAUTH2_PROXY_COOKIE_CSRF_EXPIRE=5m
      - OAUTH2_PROXY_EMAIL_DOMAINS=*

Now some time after startup of the oauth2-proxy, the key in /mnt/siteconfig/oauth2-proxy.secret gets exchanged (the resp. volume is mounted by another service, performing the exchange). It seems like the change in the file does not get automatically picked up by oauth2-proxy. Are you reading the file only once, during startup of oauth2-proxy or is it read every time a login process starts?

This is what confuses me in the doc - switching to OAUTH2_PROXY_CLIENT_SECRET with a fixed key seems to work,
but I loose the regular exchange of the client secret!

@col-panic
Copy link

col-panic commented Jan 10, 2024

client-secret-file seems not to be supported in the keycloak-oidc provider. After some research I found, that
only the oidc provider calls the respective method. I am not too familiar with go, but the oidc provider loads the resp. value here https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.5.1/providers/oidc.go while I can't find code in the keycloak-oidc provider doing this https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.5.1/providers/keycloak_oidc.go

@admincasper
Copy link

I couldn't add the client_secret_file key in my oauth2_proxy.cfg.
But I assume I can set it as a flag or environment variable? To auto-rotate secrets it's recommended to use volumes and not environment variables for secrets. So I'd like to just set the secret file path within the configmap..

oauth2_proxy.cfg: |
    oidc_issuer_url = "https://login.microsoftonline.com/${tenant_id}/v2.0"
    redirect_url = "https://${hostname}"
    email_domains = [ "*" ]
    upstreams = [ "http://${frontend.name}-service.${namespace}.svc.cluster.local:${frontend.port}" ]
    http_address = "0.0.0.0:${oauth2_port}"
    provider = "oidc"
    client_secret_file: "/mnt/secretstore/client-secret" # This gets an error

@admincasper
Copy link

admincasper commented Jan 10, 2024

Also somehow get this error:
2024/01/10 15:58:09] [main.go:43] ERROR: failed to load config: unable to load config file: open /etc/oauth2_proxy/oauth2_proxy.cfg --client-secret-file=/mnt/secretstore/client-secret: no such file or directory when I am definitely mounting the volume..

@tuunit
Copy link
Member

tuunit commented Jan 10, 2024

Hi @col-panic,

first of all, you aren't using our official docker images from quay.io/oauth2-proxy/oauth2-proxy. I have no idea what exactly bitnami is packaging on docker hub. I know they repackage a lot of open source project.

I just checked the code. We always read the client secret file when the toke redeeming is happening:

Redeem:

func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code, codeVerifier string) (*sessions.SessionState, error) {
if code == "" {
return nil, ErrMissingCode
}
clientSecret, err := p.GetClientSecret()
if err != nil {
return nil, err
}

GetClientSecret:

func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
if p.ClientSecret != "" || p.ClientSecretFile == "" {
return p.ClientSecret, nil
}
// Getting ClientSecret can fail in runtime so we need to report it without returning the file name to the user
fileClientSecret, err := os.ReadFile(p.ClientSecretFile)
if err != nil {
logger.Errorf("error reading client secret file %s: %s", p.ClientSecretFile, err)
return "", errors.New("could not read client secret file")
}
return string(fileClientSecret), nil
}

Unfortunately, the Redeem is only called on the full initial oauth2 flow and not the simple authentication path on subsequent requests.

@tuunit
Copy link
Member

tuunit commented Jan 10, 2024

client-secret-file seems not to be supported in the keycloak-oidc provider. After some research I found, that only the oidc provider calls the respective method. I am not too familiar with go, but the oidc provider loads the resp. value here https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.5.1/providers/oidc.go while I can't find code in the keycloak-oidc provider doing this https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.5.1/providers/keycloak_oidc.go

All providers should support it. The issue seems to be as described above but I haven't done a deep analysis yet.

@tuunit
Copy link
Member

tuunit commented Jan 10, 2024

I couldn't add the client_secret_file key in my oauth2_proxy.cfg. But I assume I can set it as a flag or environment variable? To auto-rotate secrets it's recommended to use volumes and not environment variables for secrets. So I'd like to just set the secret file path within the configmap..

oauth2_proxy.cfg: |
    oidc_issuer_url = "https://login.microsoftonline.com/${tenant_id}/v2.0"
    redirect_url = "https://${hostname}"
    email_domains = [ "*" ]
    upstreams = [ "http://${frontend.name}-service.${namespace}.svc.cluster.local:${frontend.port}" ]
    http_address = "0.0.0.0:${oauth2_port}"
    provider = "oidc"
    client_secret_file: "/mnt/secretstore/client-secret" # This gets an error

Your last line is wrong. You are using ":" instead of "=". It's a toml file not a yaml file.

client_secret_file = "/mnt/secretstore/client-secret" # This gets an error

@tuunit
Copy link
Member

tuunit commented Jan 10, 2024

Also somehow get this error: 2024/01/10 15:58:09] [main.go:43] ERROR: failed to load config: unable to load config file: open /etc/oauth2_proxy/oauth2_proxy.cfg --client-secret-file=/mnt/secretstore/client-secret: no such file or directory when I am definitely mounting the volume..

This looks like you made a mistake in your helm chart values.yaml. Can you share your config?

@admincasper
Copy link

Yeah, I tried to explicity set the command together with the config file in my oauth2 deployment. But it didn't work. The client_secret_file = command seems to work, but will test further. Thanks a lot for the info @tuunit !

    spec:
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: "RuntimeDefault"
        fsGroup: 101
      containers:
        - name: oauth2proxy
          image: <repo>.azurecr.io/oauth2-proxy/oauth2-proxy:stable
          args: ["--config=/etc/oauth2_proxy/oauth2_proxy.cfg", "--client-secret-file='/mnt/secretstore/client-secret'"]

@admincasper
Copy link

admincasper commented Jan 11, 2024

@tuunit Is the client_secret_file parameter by .cfg config supported for all providers?
I can only see it referenced in alpha_config code.

Should I try changing from oidc provider to azure, or will it not matter? I see that in commit
42475c2#diff-12eb03d2e4162320c9a848db9eb444ca2bbf9fc19e11d5756f85aaf1a9e38d85
the client_secret_file argument is removed and added as a legacy_option. Do I have no other options as to mount the secret file than using an environment varable?

@tuunit
Copy link
Member

tuunit commented Jan 11, 2024

Your configuration is wrong:

    spec:
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: "RuntimeDefault"
        fsGroup: 101
      containers:
        - name: oauth2proxy
          image: <repo>.azurecr.io/oauth2-proxy/oauth2-proxy:stable
          args: ["--config", "/etc/oauth2_proxy/oauth2_proxy.cfg", "--client-secret-file", "/mnt/secretstore/client-secret"]

You have essential tried to use a flag "--config=/etc/oauth2_proxy/oauth2_proxy.cfg" with a parameter of "--client-secret-file=/mnt/secretstore/client-secret".

@tuunit
Copy link
Member

tuunit commented Jan 11, 2024

@tuunit Is the client_secret_file parameter by .cfg config supported for all providers? I can only see it referenced in alpha_config code.

Should I try changing from oidc provider to azure, or will it not matter? I see that in commit 42475c2#diff-12eb03d2e4162320c9a848db9eb444ca2bbf9fc19e11d5756f85aaf1a9e38d85 the client_secret_file argument is removed and added as a legacy_option. Do I have no other options as to mount the secret file than using an environment varable?

The client_secret_file legacy option, --client-secret-file flag and clientSecretFile yaml / alpha option can be used with any provider.

@admincasper
Copy link

@tuunit Thanks for your patience. I get sporadic 403 CSRF error, sometimes adding client-id, client-secret and cookie through environment variable works. Changing to using client_secret_file option makes the error trigger again.
image

Could you take a look?
Here is my deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-oauth2-proxy
  labels:
    app.kubernetes.io/name: app-oauth2-proxy
    app.kubernetes.io/created-by: app
    app.kubernetes.io/component: frontend-oauth2-proxy
spec:
  selector:
    matchLabels:
      app: app-oauth2-proxy
  template:
    metadata:
      labels:
        app: app-oauth2-proxy
        app.kubernetes.io/name: app-oauth2-proxy
        app.kubernetes.io/created-by: app
        app.kubernetes.io/component: frontend-oauth2-proxy
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: <keyvault_mi>
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: "RuntimeDefault"
        fsGroup: 101
      containers:
        - name: oauth2proxy
          image: <acr>.azurecr.io/oauth2-proxy/oauth2-proxy:v7.5.1
          args: ["--config","/etc/oauth2_proxy/oauth2_proxy.cfg"]
          # Could comment this in instead, get CSRF error.
          # args: ["--config","/etc/oauth2_proxy/oauth2_proxy.cfg","--client-secret-file","/mnt/secretstore/client-secret"]
          livenessProbe:
            httpGet:
              path: /ping
              port: 8080
            periodSeconds: 3
            timeoutSeconds: 1
          resources:
            limits:
              cpu: 500m
              memory: 200Mi
            requests:
              cpu: 200m
              memory: 100Mi
          ports:
            - name: oauth2proxy-pod
              containerPort: 8080
          volumeMounts:
            - name: app-oauth2-proxy-config
              mountPath: /etc/oauth2_proxy/
            - name: oauth2-proxy-secrets-store
              mountPath: /mnt/secretstore
              readOnly: true
          env:
            - name: OAUTH2_PROXY_CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: oauth2-proxy-secrets-store
                  key: client-id
            - name: OAUTH2_PROXY_CLIENT_SECRET # Could comment this out
              valueFrom:
                secretKeyRef:
                  name: oauth2-proxy-secrets-store
                  key: client-secret
            - name: OAUTH2_PROXY_COOKIE_SECRET
              valueFrom:
                secretKeyRef:
                  name: oauth2-proxy-secrets-store
                  key: cookie-secret
          securityContext:
            privileged: false
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
      volumes:
        - name: app-oauth2-proxy-config
          configMap:
            name: app-oauth2-proxy
        - name: oauth2-proxy-secrets-store
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: app-oauth2-proxy

And configmap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-oauth2-proxy
data:
  oauth2_proxy.cfg: |
    oidc_issuer_url = "https://login.microsoftonline.com/${tenant_id}/v2.0"
    redirect_url = "https://${hostname}"
    email_domains = [ "*" ]
    upstreams = [ "http://app-service.${namespace}.svc.cluster.local:9898" ]
    http_address = "0.0.0.0:8080"
    provider = "oidc"
    # Could add this, get CSRF error if env variables in deployment are not added
    # client_secret_file = "/mnt/secretstore/client-secret"

    cookie_secure = true
    whitelist_domains = ".<domain>.com"
    cookie_domains = ".<domain>.com"
    cookie_expire = "4h"
    cookie_refresh = "2h"
    cookie_samesite = "lax"
    cookie_name = "__Host-app"

    pass_access_token = true
    pass_authorization_header = true
    skip_provider_button = true
    set_authorization_header = true
    set_xauthrequest = true
    skip_auth_preflight = false

    session_store_type = "redis"
    redis_connection_url = "redis://redis-master.oauth2-proxy.svc.cluster.local:6379"

@tuunit
Copy link
Member

tuunit commented Jan 16, 2024

@admincasper you might have actually found a bug then 🤔 I'll have to check the code for the differences in the flow.

@admincasper
Copy link

@tuunit The CSRF issue was fixed by removing cookie_domains = ".<domain>.com" in configmap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants