Secrets
A configuration setting that is a secret, such as a password, should be modeled with SecretConfig. It is critical to treat secrets as dynamic values, as they may be stored in an external secret provider and may be rotated at any time. The best practice is to fetch the value of the secret on demand each time it is needed. This approach ensures you always have the latest value and reduces the attack surface by minimizing the amount of time the plaintext secret is held in memory.
How to Reference Secrets Dynamically
Model with
SecretConfig: Your configuration class must contain a field of typeSecretConfig.import com.inductiveautomation.ignition.gateway.secrets.SecretConfig;
...
SecretConfig passwordSecretConfig = config.getPassword();Create a
SecretInstance: Given theSecretConfigand aGatewayContext, create aSecret<?>instance. This instance must be held onto and reused for the lifetime of the managing object, as long as the associatedSecretConfighas not changed.import com.inductiveautomation.ignition.gateway.model.GatewayContext;
import com.inductiveautomation.ignition.gateway.secrets.Secret;
import com.inductiveautomation.ignition.gateway.secrets.SecretConfig;
...
SecretConfig passwordSecretConfig = config.getPassword();
Secret<?> passwordSecret = Secret.create(gatewayContext, passwordSecretConfig);Fetch
PlaintextOn Demand: Grab a freshPlaintextvalue from theSecretinstance every time the secret is needed, using atry-with-resourcesblock to ensure the plaintext value is immediately cleared from memory.import com.inductiveautomation.ignition.gateway.secrets.Plaintext;
import com.inductiveautomation.ignition.gateway.secrets.SecretException;
// Inside a method that needs the password...
String password;
try (Plaintext plaintext = passwordSecret.getPlaintext()) {
password = plaintext.getAsString();
// Use the password here, e.g., service.login(username, password)
} catch (SecretException e) {
throw new IllegalStateException("Unable to get plaintext password from secret", e);
}
Dynamic Secret Manager Example
Below are the recommended class structures for a service manager and its factory that correctly handle dynamic secrets by holding onto the Secret<?> instance and fetching the password on demand.
Service Manager
Below is an example of a custom service manager that will handle dynamic secret fetching.
import com.inductiveautomation.ignition.gateway.secrets.Plaintext;
import com.inductiveautomation.ignition.gateway.secrets.Secret;
import com.inductiveautomation.ignition.gateway.secrets.SecretException;
class FooServiceManager {
final FooService service;
final String username;
final Secret<?> password; // Holds the Secret instance
FooServiceManager(FooService service, String username, Secret<?> password) {
this.service = service;
this.username = username;
this.password = password;
}
String login() {
// Fetches the Plaintext on demand every time login is called
try (Plaintext plaintext = password.getPlaintext()) {
return service.login(username, plaintext.getAsString());
} catch (SecretException e) {
throw new IllegalStateException("Unable to get plaintext password from secret", e);
}
}
}
Service Manager Factory
With the service manager defined in the previous example, define a service manager factory to pass the Secret instance.
import com.inductiveautomation.ignition.gateway.model.GatewayContext;
import com.inductiveautomation.ignition.gateway.secrets.Secret;
import com.inductiveautomation.ignition.gateway.secrets.SecretConfig;
class FooServiceManagerFactory {
final GatewayContext gatewayContext;
final FooService service;
FooServiceManagerFactory(GatewayContext gatewayContext, FooService service) {
this.gatewayContext = gatewayContext;
this.service = service;
}
FooServiceManager create(FooServiceManagerConfig config) {
String username = config.username();
SecretConfig passwordSecretConfig = config.password();
Secret<?> passwordSecret = Secret.create(gatewayContext, passwordSecretConfig);
// Returns the manager with the Secret instance, not the Plaintext
return new FooServiceManager(service, username, passwordSecret);
}
}
Takeaways for Dynamic Secrets
- Do hold onto and reuse the instance of
Secretfor the lifetime of the managing object as long as the associatedSecretConfighas not changed. - Do grab a fresh
Plaintextvalue from theSecreton demand every time it is needed. - Do not grab the
Plaintextvalue from theSecretonce at the beginning of the lifetime of the managing object and reuse the same value, as the secret may be dynamic.
Keep in mind that a SecretException could be thrown every time you try to grab a fresh Plaintext value from the Secret instance. Design your systems to be resilient to secrets-related failures, such as a network issue communicating with an external SecretProvider.
Third Party Libraries and Static Secrets
There may be situations where you are using a third party library that treats secrets as static configuration values. For example, when a new JDBC connection in the database connections system fails due to an exception, the system is designed to re-create the static configuration properties and will grab a fresh value of the password from the Secret. This allows the system to recover from rotated passwords thanks to existing retry mechanisms.
ResourceReference and SecretConfig
Referenced secrets are instances of the SecretConfig.Referenced type that contain the name of the Secret Provider and the name of the secret in that provider. As the Secret Provider name is a reference to a SecretProvider extension point resource, it is a great candidate for a ResourceReference. Implementing ResourceReferenceDelegate to return ResourceReferences instances for these properties ensures that referenced secrets prevent accidental deletion or renaming of the providers they reference, and are updated in the event of a rename if the user chooses.
addReferenceProperty(
"voipGateway.password",
SecretReferenceProperty.<SipNotificationProfileConfig>builder()
.setGetSecretConfigFunction(SipNotificationProfileConfig::getPassword)
.setUpdateSecretConfigFunction(SipNotificationProfileConfig::setPassword)
.build());