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

Support providing root client ID via env. variables when bootstrapping #422

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

dimas-b
Copy link
Contributor

@dimas-b dimas-b commented Nov 1, 2024

Allow the root client ID and secrets to be provided via environment variables when bootstrapping.

For "in memory" use cases, this mean the main Polaris server will read user-provided root secrets from the env., if propvided.

For "persistent" use cases, the bootstrap command will read user-provided root secrets from the env., if propvided.

The env. variables are:

  • POLARIS_BOOTSTRAP_<REALM>_ROOT_CLIENT_ID
  • POLARIS_BOOTSTRAP_<REALM>_ROOT_CLIENT_SECRET

If these variables are not provided, random values are generated as before.

How Has This Been Tested?

Manual smoke tests.

@eric-maynard
Copy link
Contributor

I recall that @collado-mike and I discussed this as one option when bootstrapping, but I can't remember why we went with the current approach.

For my part, I would ideally like to see as little a difference in the UX of the different metastores as possible.

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 4, 2024

@eric-maynard :

I would ideally like to see as little a difference in the UX of the different metastores as possible.

Do you mean supporting the same overrides under the bootstrap command? I can certainly add that. WDYT?

@adutra
Copy link
Contributor

adutra commented Nov 4, 2024

Taking a step back, and thinking about real-life scenarios: how is an operator supposed to get hold of the root credentials, after installing and bootstrapping Polaris? I read the Polaris documentation about production setups, it does cover bootstrapping, but it doesn't cover this detail in particular, so I guess the exact procedure depends on the metastore being used? Wouldn't it be possible to standardize that?

I'm also raising this point because while working on the Quarkus port, I realized that it is not possible to create a true integration test currently, that is, a test that spawns Polaris as a black box, and only interacts with it through its external APIs. This is because once started and bootstrapped, and regardless of the metastore used, the test is unable to infer which credentials to use to communicate with Polaris.

All in all, I wonder if we aren't missing a simple and standard way to pass the root credentials to the boostrapping process, even in production setups – or alternatively, a way to retrieve those, that would also be standardized across metastores.

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 4, 2024

[...] I wonder if we aren't missing a simple and standard way to pass the root credentials to the boostrapping process.

My personal take on that is that the bootstrap command should either take root credentials as a parameters, or output the uniquely generated root credentials. I think we can support both options (e.g. as CLI arguments) and I'm willing to add that to this PR if other developers agree.

@eric-maynard
Copy link
Contributor

A few thoughts here.

  1. For testing, I've found it generally sufficient just to use e.g. --access-token 'principal:root;realm:default-realm', so you shouldn't actually need root credentials in many cases unless you are testing a context resolver.
  2. In general, I feel nervous about allowing the bootstrap process to use non-random credentials (even when supplied by the user). I am not a security expert but this seems insecure.
  3. With respect to the bootstrap command printing the credentials to stdout, this was thought to be insecure. I remember now that this explicitly came up in my previous conversations with @collado-mike

Copy link
Member

@jbonofre jbonofre left a comment

Choose a reason for hiding this comment

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

I guess we can integrate this in the TestEnvironmentExtension recently added (waiting Quarkus 😄 ).

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 5, 2024

@eric-maynard : thanks for sharing your views on this matter. Re: security of bootstrapping, I did not mean printing credentials to STDOUT. The "output" can be a file at a user-specified location. I think this is quite similar to downloading ssh certificates for a VM from a cloud vendor.

I wonder what options end users currently have for bootstrapping, though 🤔 How will a user be able to discover a generated root credential? (Apologies if I missed this in somewhere in docs).

@adutra
Copy link
Contributor

adutra commented Nov 5, 2024

I agree that we need to proceed carefully since it's a security-related issue.

I'd note, fwiw, that Keycloak allows to bootstrap the root credentials via environment variables.

I don't think we can suspect Keycloak of taking security lightly :-)

In another place of Keycloak docs, we can learn more about how the environment variables are processed:

If the initial administrator already exists and the environment variables are still present at startup, an error message stating the failed creation of the initial administrator is shown in the logs. Keycloak ignores the values and starts up correctly.

Couldn't we do something similar to that?

@adutra
Copy link
Contributor

adutra commented Nov 5, 2024

The "output" can be a file at a user-specified location.

If we go down this route, maybe check that the file permissions are as restrictive as possible.

@eric-maynard
Copy link
Contributor

eric-maynard commented Nov 5, 2024

@dimas-b

I wonder what options end users currently have for bootstrapping, though 🤔 How will a user be able to discover a generated root credential? (Apologies if I missed this in somewhere in docs).

So if you're just doing testing like this PR would address, you can just use passwordless auth. You normally don't need the root credentials.

However if you're asking more generally how to retrieve the root credentials it is metastore-dependent. Ideally your metastore is set up with your auth provider and allows you to do something nice like set/retrieve the credentials using SSO. Or perhaps they are exposed through some API which is already secured. But in the simplest case where you're just using a postgres metastore, you can retrieve them through something like:

SELECT principalclientid, mainsecret FROM principal_secrets;

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 5, 2024

@eric-maynard :

So if you're just doing testing like this PR would address, you can just use passwordless auth. You normally don't need the root credentials.

I could use principal:root;realm:default-realm as an access token, but I happen to need to use the client credentials auth flow, which currently uses random values for the root user. Discovering the random credentials currently involves scanning Polaris STDOUT, which is inconvenient. I'd like to have control over the inputs I provide to Polaris.

So this PR proposes to make this an option for the user to define the root credential if the user so chooses. I think it could be convenient for other people too.

As for the general bootstrapping case, the above discussion is interesting, but maybe we can continue that on the dev list or separate PR. As for this PR, do you think the idea of allowing user overrides for root credentials is reasonable given that it only applies to the "test" authentication implementation, which is already not "secret" given the fixed root token?

@eric-maynard
Copy link
Contributor

I see. If you want to test the auth flow and you can't call bootstrapRealms then I can see how the current bootstrap process is not great.

Could you call rotatePrincipalSecrets using the passwordless auth to receive new credentials?

Barring that, I am open to allowing the user to set the credentials using an env variable for the in-memory metastore.

@collado-mike
Copy link
Contributor

It is a fairly common and accepted practice to generate random secrets during bootstrapping - e.g., terraform has built-in support for this - https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password . Allowing Terraform to randomly generate a password, bootstrap Polaris, and stick the secrets in Vault or k8s secrets is a very secure pattern and something I think we ought to support.

@adutra
Copy link
Contributor

adutra commented Nov 8, 2024

It is a fairly common and accepted practice to generate random secrets during bootstrapping [..]

And I'd note that that is the usual pattern that most Helm charts adopt when bootstrapping things like databases.

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 8, 2024

SGTM, based on the above discussion, I think I'll extend the env. variables support to the normal bootstrap command too.

@dimas-b dimas-b marked this pull request as draft November 8, 2024 14:16
@dimas-b dimas-b marked this pull request as ready for review November 8, 2024 18:05
@dimas-b dimas-b requested a review from adutra as a code owner November 8, 2024 18:05
@dimas-b dimas-b changed the title Support providing root client ID via env. variables for testing Support providing root client ID via env. variables when bootstrapping Nov 8, 2024
public synchronized Map<String, PolarisMetaStoreManager.PrincipalSecretsResult> bootstrapRealms(
List<String> realms) {
public Map<String, PolarisMetaStoreManager.PrincipalSecretsResult> bootstrapRealms(
List<String> realms, Function<String, PrincipalSecretsGenerator> rootSecretsPerRealm) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the PrincipalSecretsGenerator. Is it necessary to have it per realm, though? Can it take the realm as a method argument instead

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'd probably have to add realm to PolarisCallContext.

From my POV, I wanted to avoid exposing realm to lower-level code for better abstraction.

In practice only one realm is normally bootstrapped at a time, so we can probably drop the realm parameter completely and use env. vars. like POLARIS_BOOTSTRAP_ROOT_CLIENT_ID. WDYT?

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've reworked the generator code a bit. Hope it's clearer wrt realms now.

Comment on lines 706 to 707
public @NotNull BaseResult bootstrapPolarisService(
@NotNull PolarisCallContext callCtx, PrincipalSecretsGenerator rootSecretsGenerator) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than changing the public interface, can we make the PrincipalSecretsGenerator a constructor param? I don't think callers need to know anything about this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Different generators are used during bootstrapping and (REST) API-driven principal creation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reworked to avoid modifying public interfaces.

Comment on lines 450 to 454
public @NotNull PolarisPrincipalSecrets generateNewPrincipalSecrets(
@NotNull PolarisCallContext callCtx, @NotNull String principalName, long principalId) {
@NotNull PolarisCallContext callCtx,
@NotNull String principalName,
long principalId,
PrincipalSecretsGenerator generator) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here - rather than passing in the generator as an argument, I think it should be a constructor argument

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reworked to avoid modifying public interfaces.

Copy link
Contributor

@collado-mike collado-mike left a comment

Choose a reason for hiding this comment

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

Change looks good to me - some javadocs would be helpful, though

import org.jetbrains.annotations.NotNull;

@FunctionalInterface
public interface PrincipalSecretsGenerator {
Copy link
Contributor

Choose a reason for hiding this comment

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

Short javadoc 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.

added

Comment on lines 36 to 44
static Realms bootstrap() {
return bootstrap(System.getenv()::get);
}

static Realms bootstrap(Function<String, String> config) {
return new Realms(config);
}

class Realms {
Copy link
Contributor

Choose a reason for hiding this comment

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

Some javadocs here would be useful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

* from services that actually manage principal objects (create, remove, rotate secrets, etc.)
*
* <p>The implementation statically available from {@link #bootstrap()} allows one-time client ID
* and secret overrides via environment variables, which can be useful for bootstrapping new realms.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we call out exactly what environment variables are used, here and preferably in the docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

@@ -58,11 +60,17 @@ public abstract class LocalPolarisMetaStoreManagerFactory<StoreType>
private static final Logger LOGGER =
LoggerFactory.getLogger(LocalPolarisMetaStoreManagerFactory.class);

private final PrincipalSecretsGenerator.Realms secretsGenerator = bootstrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks a bit odd, why is a secretsGenerator of the type Realms?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, it's like a secretsGeneratorGenerator. I wonder if we can just get rid of the thin Realms type and push this into PrincipalSecretsGenerator

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 believe this will require PolarisMetaStoreSession implementations to have "realm" as a field. Would that be ok from your POV?.. Currently sessions are not strongly bound to realms. Realm-specific behaviour is relevant only to the bootstrap command and factories.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, the places calling produceSecrets don't necessarily have access to the realm. Maybe we can just move this into the constructor of PrincipalSecretsGenerator itself

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 reworked the impl. a bit... hopefully the bootstrapping logic is clearer now.

@eric-maynard: Would you still prefer to have a non-lambda class for PrincipalSecretsGenerator implementations?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's fine as it is now, maybe in the future if there are more implementations we can refine it further. Thanks for iterating on this!

Comment on lines +65 to +69
static PrincipalSecretsGenerator bootstrap(String realmName) {
return bootstrap(realmName, System.getenv()::get);
}

static PrincipalSecretsGenerator bootstrap(String realmName, Function<String, String> config) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks a lot better, thanks!

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 20, 2024

I believe that after #438 but without this PR it is not really possible to bootstrap Polaris with EclipseLink... I do not see a way to discover the auto-generated root secret 🤔 ;)

@eric-maynard
Copy link
Contributor

eric-maynard commented Nov 20, 2024

+1 @dimas-b, I think this is a partial fix for that problem. However we probably shouldn't allow bootstrapping without specifying credentials if the intent is that users can retrieve secrets from the metastore but we no longer put them in the metastore.

I still think we can merge this as-is and follow up with that potential restriction.

Other possibilities for resolving this issue:

  1. Make EclipseLink boostrap print secrets like in-memory bootstrap does
  2. Make it so that we persist plaintext secrets for primary secrets but not secondary secrets
  3. Hack Do not persist plaintext secrets in the metastore #438 so that (2) applies only to root's first-time secret, and enforce rotation on root

Any preferences @dimas-b / @collado-mike ?

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 20, 2024

My preference would be to keep the env. variables for root password bootstrapping, plus add CLI options to the bootstrap command to print the auto-generated password to STDOUT, but only at user's request.

By the way, I'm rebasing this PR to resolve conflicts... will probably squash too.

@dimas-b dimas-b force-pushed the root-id-override-for-test branch 2 times, most recently from d88d74a to 5fdf9b8 Compare November 20, 2024 21:53
@eric-maynard
Copy link
Contributor

eric-maynard commented Nov 21, 2024

plus add CLI options to the bootstrap command to print the auto-generated password to STDOUT, but only at user's request.

I like this idea, and I can do this -- but do you think we should fail the bootstrap if

  1. The env variables are not set
  2. credential printing is disabled?

In this case the metastore is pretty much bricked, so I think we should not proceed with the bootstrap.

Edit: See my sketch of this idea here, which would be rebased on this PR.

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 21, 2024

but do you think we should fail the bootstrap if

The env variables are not set
credential printing is disabled? [...]

SGTM 👍

@dimas-b
Copy link
Contributor Author

dimas-b commented Nov 22, 2024

@collado-mike : are you ok to merge?

@@ -113,7 +115,8 @@ public PolarisEclipseLinkMetaStoreSessionImpl(
@NotNull PolarisStorageIntegrationProvider storageIntegrationProvider,
@NotNull RealmContext realmContext,
@Nullable String confFile,
@Nullable String persistenceUnitName) {
@Nullable String persistenceUnitName,
PrincipalSecretsGenerator secretsGenerator) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@NotNull annotation, since we don't do any null checking below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

Introduce a `PrincipalSecretsGenerator` interface to isolate secrets
generation from principal management code.

Update meta store factories to allow the user to define the root
client ID and secret via environment variables during bootstrapping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants