Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ org.killbill.billing.plugin.stripe.chargeStatementDescriptor=ZZZ' \
http://127.0.0.1:8080/1.0/kb/tenants/uploadPluginConfig/killbill-stripe
```

## Securing API Keys

By default, API keys are stored in plaintext in Kill Bill's tenant configuration. You can keep secrets out of the database by using environment variable references.

### Environment variable references

Store your Stripe keys in environment variables and reference them in the plugin config:

```
org.killbill.billing.plugin.stripe.apiKey=${env:STRIPE_API_KEY}
org.killbill.billing.plugin.stripe.publicKey=${env:STRIPE_PUBLIC_KEY}
```

This way, secrets never enter the database and can be managed by your orchestration layer (K8s Secrets, HashiCorp Vault, AWS SSM, etc.).

For multi-tenant setups, each tenant can reference a different environment variable:

```
# Tenant A config
org.killbill.billing.plugin.stripe.apiKey=${env:STRIPE_KEY_TENANT_A}

# Tenant B config
org.killbill.billing.plugin.stripe.apiKey=${env:STRIPE_KEY_TENANT_B}
```

### Migration note

Existing plaintext configurations continue to work with zero changes. Environment variable references are fully opt-in.

## Payment Method flow

To charge a payment instrument (card, bank account, etc.), you first need to collect the payment instrument details in Stripe and create an associated payment method in Kill Bill.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public class StripeConfigProperties {

public StripeConfigProperties(final Properties properties, final String region) {
this.region = region;
this.apiKey = properties.getProperty(PROPERTY_PREFIX + "apiKey");
this.publicKey = properties.getProperty(PROPERTY_PREFIX + "publicKey");
this.apiKey = StripeConfigPropertyResolver.resolve(properties.getProperty(PROPERTY_PREFIX + "apiKey"));
this.publicKey = StripeConfigPropertyResolver.resolve(properties.getProperty(PROPERTY_PREFIX + "publicKey"));
this.apiBase = properties.getProperty(PROPERTY_PREFIX + "apiBase");
this.proxyHost = properties.getProperty(PROPERTY_PREFIX + "proxyHost");
this.proxyPort = Integer.parseInt(properties.getProperty(PROPERTY_PREFIX + "proxyPort", "-1"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2020-2020 Equinix, Inc
* Copyright 2014-2020 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.killbill.billing.plugin.stripe;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

/**
* Resolves config property values that may be plaintext or environment variable
* references ({@code ${env:VAR_NAME}}).
*
* <ul>
* <li><b>Plaintext</b> — returned as-is (backward compatible)</li>
* <li><b>{@code ${env:VAR_NAME}}</b> — resolved via {@link System#getenv(String)}</li>
* </ul>
*/
public class StripeConfigPropertyResolver {

private static final Pattern ENV_PATTERN = Pattern.compile("^\\$\\{env:([^}]+)}$");

/**
* Resolve a configuration value. Supports plaintext and {@code ${env:VAR}}.
*
* @param value the raw property value (may be {@code null})
* @return the resolved plaintext value, or {@code null} if input is {@code null}
*/
public static @Nullable String resolve(@Nullable final String value) {
if (value == null) {
return null;
}

final Matcher envMatcher = ENV_PATTERN.matcher(value);
if (envMatcher.matches()) {
final String varName = envMatcher.group(1);
final String envValue = resolveEnvVar(varName);
if (envValue == null) {
throw new IllegalStateException("Environment variable '" + varName + "' is not set");
}
return envValue;
}

return value;
}

// Package-private for test overriding
static @Nullable String resolveEnvVar(final String name) {
return System.getenv(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2020-2020 Equinix, Inc
* Copyright 2014-2020 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.killbill.billing.plugin.stripe;

import java.util.Properties;

import org.testng.Assert;
import org.testng.annotations.Test;

public class TestStripeConfigPropertyResolver {

@Test(groups = "fast")
public void testPlaintextPassthrough() {
Assert.assertEquals(StripeConfigPropertyResolver.resolve("sk_test_XXX"), "sk_test_XXX");
Assert.assertEquals(StripeConfigPropertyResolver.resolve("plain value"), "plain value");
Assert.assertEquals(StripeConfigPropertyResolver.resolve(""), "");
}

@Test(groups = "fast")
public void testNullPassthrough() {
Assert.assertNull(StripeConfigPropertyResolver.resolve(null));
}

@Test(groups = "fast")
public void testEnvVarResolution() {
// HOME is always set on Unix/macOS; use PATH as fallback
final String varName = System.getenv("HOME") != null ? "HOME" : "PATH";
final String expected = System.getenv(varName);
Assert.assertNotNull(expected, varName + " should be set in the test environment");
Assert.assertEquals(StripeConfigPropertyResolver.resolve("${env:" + varName + "}"), expected);
}

@Test(groups = "fast", expectedExceptions = IllegalStateException.class,
expectedExceptionsMessageRegExp = ".*Environment variable.*is not set.*")
public void testEnvVarNotSetFails() {
// Use a var name that is extremely unlikely to exist
StripeConfigPropertyResolver.resolve("${env:KILLBILL_STRIPE_TEST_NONEXISTENT_VAR_12345}");
}

@Test(groups = "fast")
public void testBackwardCompatibilityWithStripeConfigProperties() {
final Properties properties = new Properties();
properties.setProperty("org.killbill.billing.plugin.stripe.apiKey", "sk_test_plaintext");
properties.setProperty("org.killbill.billing.plugin.stripe.publicKey", "pk_test_plaintext");

final StripeConfigProperties config = new StripeConfigProperties(properties, "");

Assert.assertEquals(config.getApiKey(), "sk_test_plaintext");
Assert.assertEquals(config.getPublicKey(), "pk_test_plaintext");
}
}
Loading