Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[extension/oauth2clientauth] Enable dynamically reading ClientID and ClientSecret from command #32623

Closed
wants to merge 14 commits into from
Closed

[extension/oauth2clientauth] Enable dynamically reading ClientID and ClientSecret from command #32623

wants to merge 14 commits into from

Conversation

elikatsis
Copy link
Contributor

@elikatsis elikatsis commented Apr 23, 2024

Description:
This PR implements the feature described in detail in the issue linked below.

In a nutshell, it extends the oauth2clientauth extension to retrieve ClientID and/or ClientSecret by executing a command whenever a new token is needed for the OAuth flow.
As a result, the extension can use updated credentials (when the old ones expire and they get rotated for example) without the need to restart the OTEL collector, as long as the command returns a different output (e.g., fetch credentials from a secret store).

As part of this PR I also modify the design of the code which is responsible to get the actual client ID and secret values based on an old review comment. I have exposed the details in the tracking issue.

Link to tracking Issue: Closes #32602

Testing:

  • Unit testing: Extended the unit tests to ensure the functionality
  • Tested in realistic environments both as a systemd service and a docker container (exporting data with the otlphttp exporter)
    The collectors export data to a service which sits behind and OIDC authentication proxy and the oauth2clientauth extension successfully retrieves the credentials from a secret store and authenticates against the service.

Documentation:
I have extended the extension's README file.

@elikatsis
Copy link
Contributor Author

Friendly ping @pavankrish123 @jpkrohling for visibility.
WDYT?

Copy link
Member

@jpkrohling jpkrohling left a comment

Choose a reason for hiding this comment

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

I feel like some changes to the tests weren't necessary, but this looks good. I'd still like to have extra 👀 from other @open-telemetry/collector-contrib-approvers, given this code spanws arbitrary commands to get values. This could be used as vector together with another vulnerability, to send confidential data to an attacker by making a curl call to a property controlled by the attacker.

I would probably prefer to allow only one argument to the cmd, requiring admins to wrap a cmd+args in a shell script on the machine. This way, we are certain that only those commands are executed.

@@ -125,8 +131,6 @@ func TestOAuthClientSettingsCredsConfig(t *testing.T) {
settings: &Config{
ClientIDFile: testCredsFile,
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Copy link
Member

Choose a reason for hiding this comment

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

What happened here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PTAL at my answer at your other comment where I drop all transport TLS settings -related assertions from this test.

@@ -186,13 +228,6 @@ func TestOAuthClientSettingsCredsConfig(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, test.expectedClientConfig.ClientID, cfg.ClientID)
assert.Equal(t, test.expectedClientConfig.ClientSecret, cfg.ClientSecret)

// test tls settings
transport := rc.client.Transport.(*http.Transport)
Copy link
Member

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test TestOAuthClientSettingsCredsConfig, I introduced it in my previous contribution (#26310). I included the TLS settings as a copying-pasting artifact, but I shouldn't have them since it's duplication.
We are testing the transport and TLS configuration [more extensively] in the TestOAuthClientSettings test (current source).
I removed the // test tls settings code to have a single place to test TLS settings (could even be split to a separate test) and, if anything changes on the TLS front, drop the need to touch this unrelated test (for example #32332, #32394).

Moreover, related to your other comment about removing TokenURL and Scopes assignments, my previous contribution had to assign a value to the TokenURL field because at that point we've been validating the configuration when calling newClientAuthenticator(), and a missing TokenURL got this test breaking [kind of falsely].
But, later, #30469 removed this validation logic. So we don't need to configure it anymore for these tests, and I deleted the relevant lines to drop the [now] unnecessary boilerplate.

I'll bring them back if you believe they should be there.

Copy link
Member

@jpkrohling jpkrohling May 8, 2024

Choose a reason for hiding this comment

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

No, I think it's fine, thank you for the explanation. My concern was whether the new code introduced changes in how we handle TLS, which wasn't apparent at first.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, can you revert this change, and send a new PR removing it? Just so that we isolate the changes, ensuring they are not related.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had it in a separate commit. I pushed a reversion commit. I'll send a new PR when we move forward with this one.

cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204
outBytes, err := cmd.Output()
if err != nil {
return "", err
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return "", err
return "", fmt.Errorf("failed to execute the command %q to obtain credential details: %v", err)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense! I'll push a fixup commit since your suggestion is missing the strings.Join() for %q and also use %w instead of %v.

@@ -75,10 +75,16 @@ Following are the configuration fields
- **client_id_file** - The file path to retrieve the client identifier issued to the client.
The extension reads this file and updates the client ID used whenever it needs to issue a new token. This enables dynamically changing the client credentials by modifying the file contents when, for example, they need to rotate. <!-- Intended whitespace for compact new line -->
This setting takes precedence over `client_id`.
- **client_id_cmd** - The command to retrieve the client identifier issued to the client. Expects a list of strings.
Copy link
Member

Choose a reason for hiding this comment

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

Is this the only list of strings we have on this file? If so, please provide an example. Add another example in the testdata's config as well please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also have the scopes field as a list of strings. Nevertheless, I'll push an extra commit to showcase this.

Copy link
Contributor Author

@elikatsis elikatsis left a comment

Choose a reason for hiding this comment

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

@jpkrohling thanks a lot for your review!

I pushed two extra commits to address your comments and replied on your comments for the changes in tests.

I would probably prefer to allow only one argument to the cmd, requiring admins to wrap a cmd+args in a shell script on the machine. This way, we are certain that only those commands are executed.

I understand your concerns. However, it's not trivial to do this cmd+args wrapping when deploying the collector as a container, or it would require extra effort and not to use the official community image out-of-the-box.
When deploying the OTEL collector in some other way which allows the software to access a binary from the host filesystem (e.g., a systemd service), a vulnerability which enables the attacker to modify this extension's pointer and run arbitrary commands is probably the last concern since it sounds like there's a bigger security hole in the system.
I am no security expert, though, so I could be very off here :p

I'll also take a look at the JMX receiver you mentioned in the issue.

@@ -125,8 +131,6 @@ func TestOAuthClientSettingsCredsConfig(t *testing.T) {
settings: &Config{
ClientIDFile: testCredsFile,
ClientSecret: "testsecret",
TokenURL: "https://example.com/v1/token",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PTAL at my answer at your other comment where I drop all transport TLS settings -related assertions from this test.

@@ -186,13 +228,6 @@ func TestOAuthClientSettingsCredsConfig(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, test.expectedClientConfig.ClientID, cfg.ClientID)
assert.Equal(t, test.expectedClientConfig.ClientSecret, cfg.ClientSecret)

// test tls settings
transport := rc.client.Transport.(*http.Transport)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test TestOAuthClientSettingsCredsConfig, I introduced it in my previous contribution (#26310). I included the TLS settings as a copying-pasting artifact, but I shouldn't have them since it's duplication.
We are testing the transport and TLS configuration [more extensively] in the TestOAuthClientSettings test (current source).
I removed the // test tls settings code to have a single place to test TLS settings (could even be split to a separate test) and, if anything changes on the TLS front, drop the need to touch this unrelated test (for example #32332, #32394).

Moreover, related to your other comment about removing TokenURL and Scopes assignments, my previous contribution had to assign a value to the TokenURL field because at that point we've been validating the configuration when calling newClientAuthenticator(), and a missing TokenURL got this test breaking [kind of falsely].
But, later, #30469 removed this validation logic. So we don't need to configure it anymore for these tests, and I deleted the relevant lines to drop the [now] unnecessary boilerplate.

I'll bring them back if you believe they should be there.

cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204
outBytes, err := cmd.Output()
if err != nil {
return "", err
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense! I'll push a fixup commit since your suggestion is missing the strings.Join() for %q and also use %w instead of %v.

@@ -75,10 +75,16 @@ Following are the configuration fields
- **client_id_file** - The file path to retrieve the client identifier issued to the client.
The extension reads this file and updates the client ID used whenever it needs to issue a new token. This enables dynamically changing the client credentials by modifying the file contents when, for example, they need to rotate. <!-- Intended whitespace for compact new line -->
This setting takes precedence over `client_id`.
- **client_id_cmd** - The command to retrieve the client identifier issued to the client. Expects a list of strings.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also have the scopes field as a list of strings. Nevertheless, I'll push an extra commit to showcase this.

elikatsis added 14 commits May 8, 2024 15:47
Introduce a constructor for clientCredentialsConfig to make the object
details more abstract to the rest of the extension.
In preparation for a minor design enhancement, introduce value sources
for raw values and values read from files.
Extend the clientCredentialsConfig object with value sources for
ClientID and ClientSecret. It does not make actual use of them yet.
Use the recent changes related to value sources to retrieve the client
ID and secret during the final Config creation.
Extend the Config object to expect 'client_id_cmd' and
'client_secret_cmd' values as lists of strings to retrieve the client ID
and/or secret by executing commands.
Drop parts related to TLS settings and token retrieval already verified
in previous tests.
This reverts commit bc4f2c210e70df1520cd6116a9deaa5b6c29b1cf.
@jpkrohling
Copy link
Member

a vulnerability which enables the attacker to modify this extension's pointer and run arbitrary commands is probably the last concern since it sounds like there's a bigger security hole in the system.

A successful attack is typically the result of using a few attack vectors to exploit a vulnerability. So, while this likely wouldn't introduce a vulnerability, it could be used as a vector in an attack.

If other approvers are OK, with this change, I'm also OK in accepting it.

jpkrohling pushed a commit that referenced this pull request May 16, 2024
**Description:**

Drop duplicate testing of parts related to TLS settings which already
verified more extensively in other tests. This led to relevant changes
requiring identical modifications in multiple places (e.g., #32332,
 #32394).

Also remove 'TokenURL' and 'Scopes' from the corresponding configs as
they are not required anymore (since #30469).

For more details, see also

#32623 (comment)

Signed-off-by: Ilias Katsakioris <ilias.katsakioris@hpe.com>
Copy link
Contributor

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions bot added the Stale label May 23, 2024
@elikatsis
Copy link
Contributor Author

Not stale

@github-actions github-actions bot removed the Stale label May 24, 2024
Copy link
Contributor

github-actions bot commented Jun 7, 2024

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions bot added the Stale label Jun 7, 2024
@elikatsis
Copy link
Contributor Author

@jpkrohling any update on this?

@github-actions github-actions bot removed the Stale label Jun 8, 2024
@jpkrohling
Copy link
Member

I haven't heard from other approvers. Would you mind joining the SIG Collector and bringing this topic there? Or perhaps ask on #otel-collector on Slack. I'm still not sure I approve this change, at least not until I have more time to think about the possible attack scenarios. If other approvers have a different opinion and want to approve this instead, I won't block it.

Copy link
Contributor

github-actions bot commented Jul 3, 2024

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions bot added the Stale label Jul 3, 2024
@jpkrohling
Copy link
Member

@elikatsis, I'm sorry for this taking so long, but I think we'll need to close this. I'm VERY happy about your engagement and proposal, and I think this suggestion goes in the right direction. However, I'm not 100% comfortable with allowing this component to execute arbitrary commands on the server. In general, I agree that there's a bigger concern if the Collector can be tricked into doing that, but this feature could be used as a vector in a bigger vulnerability.

My recommendation for your specific case (although I suspect you are doing this already): host a version of this component on your own repository, and build your distribution with this component. This way, you can use this feature and distribute it to other users, even if it's not part of the official distribution. Yes, it's a bit more effort, but I think it's justified given the use-case.

Again, I'm sorry for closing this, but I think it's the right thing for the moment.

@elikatsis
Copy link
Contributor Author

@jpkrohling thank you for taking the time to consider this.

Unfortunately I didn't make it to start a discussion on Slack or a community meeting in time. But, no matter the outcome, it's been a great back and forth, both in this and my previous contribution.

Keep up the good work!

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.

[extension/oauth2clientauth] Enable dynamically reading ClientID and ClientSecret from command
3 participants