Skip to content

Commit 051b3d4

Browse files
committed
feat: support to re-creating the SSL cluster certificate before it expires
1 parent 28aaee8 commit 051b3d4

11 files changed

Lines changed: 135 additions & 18 deletions

File tree

stackgres-k8s/e2e/spec/ssl

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,24 @@ e2e_test() {
3535
generated_ssl_check() {
3636
create_or_replace_cluster "$CLUSTER_NAME" "$CLUSTER_NAMESPACE" "2" \
3737
--set cluster.postgres.ssl.enabled=true \
38+
--set-string cluster.postgres.ssl.duration=P1DT2M \
3839
--set cluster.postgres.ssl.certificateSecretKeySelector=null \
3940
--set cluster.postgres.ssl.privateKeySecretKeySelector=null
4041

41-
wait_until kubectl exec -n "$CLUSTER_NAMESPACE" "$CLUSTER_NAME-0" -c patroni -- ls /etc/ssl/tls.key
42+
SSL_KEY_FILE="$(wait_until kubectl exec -n "$CLUSTER_NAMESPACE" "$CLUSTER_NAME-0" -c patroni -- ls -l /etc/ssl/tls.key)"
43+
44+
ssl_check
45+
46+
wait_until eval '! kubectl exec -n "$CLUSTER_NAMESPACE" "$CLUSTER_NAME-0" -c patroni -- ls -l /etc/ssl/tls.key | grep -qF "$(printf %s "$SSL_KEY_FILE" | cut -d " " -f 2-)"'
47+
48+
NEW_SSL_KEY_FILE="$(wait_until kubectl exec -n "$CLUSTER_NAMESPACE" "$CLUSTER_NAME-0" -c patroni -- ls -l /etc/ssl/tls.key)"
49+
50+
if [ "$NEW_SSL_KEY_FILE" != "$SSL_KEY_FILE" ]
51+
then
52+
success "SSL certificate was re-newed"
53+
else
54+
fail "SSL certificate was NOT re-newed"
55+
fi
4256

4357
ssl_check
4458
}

stackgres-k8s/src/common/src/main/java/io/stackgres/common/PatroniUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ static String getInitialConfig(
271271
kubernetes.put("pod_ip", "${POD_IP}");
272272
kubernetes.set("ports", getPatroniEndpointPortsAsJson(cluster, objectMapper));
273273
if (config.get("kubernetes") instanceof ObjectNode) {
274-
Seq.seq(config.get("kubernetes").fields())
274+
Seq.seq(config.get("kubernetes").properties())
275275
.filter(field -> !kubernetes.has(field.getKey()))
276276
.forEach(field -> kubernetes.set(field.getKey(), field.getValue()));
277277
}

stackgres-k8s/src/common/src/main/java/io/stackgres/common/crd/sgcluster/StackGresClusterSsl.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55

66
package io.stackgres.common.crd.sgcluster;
77

8+
import java.time.format.DateTimeParseException;
89
import java.util.Objects;
910

11+
import com.fasterxml.jackson.annotation.JsonIgnore;
1012
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
1113
import com.fasterxml.jackson.annotation.JsonInclude;
1214
import io.quarkus.runtime.annotations.RegisterForReflection;
1315
import io.stackgres.common.StackGresUtil;
1416
import io.stackgres.common.crd.SecretKeySelector;
17+
import io.stackgres.common.validation.FieldReference;
18+
import io.stackgres.common.validation.FieldReference.ReferencedField;
1519
import io.sundr.builder.annotations.Buildable;
1620
import jakarta.validation.Valid;
21+
import jakarta.validation.constraints.AssertTrue;
1722

1823
@RegisterForReflection
1924
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@@ -31,6 +36,26 @@ public class StackGresClusterSsl {
3136
@Valid
3237
private SecretKeySelector privateKeySecretKeySelector;
3338

39+
private String duration;
40+
41+
@ReferencedField("duration")
42+
interface Duration extends FieldReference { }
43+
44+
@JsonIgnore
45+
@AssertTrue(message = "duration must be positive and in ISO 8601 duration format:"
46+
+ " `PnDTnHnMn.nS`.",
47+
payload = Duration.class)
48+
public boolean isBackupNewerThanValid() {
49+
try {
50+
if (duration != null) {
51+
return !java.time.Duration.parse(duration).isNegative();
52+
}
53+
return true;
54+
} catch (DateTimeParseException ex) {
55+
return false;
56+
}
57+
}
58+
3459
public Boolean getEnabled() {
3560
return enabled;
3661
}
@@ -55,6 +80,14 @@ public void setPrivateKeySecretKeySelector(SecretKeySelector privateKeySecretKey
5580
this.privateKeySecretKeySelector = privateKeySecretKeySelector;
5681
}
5782

83+
public String getDuration() {
84+
return duration;
85+
}
86+
87+
public void setDuration(String duration) {
88+
this.duration = duration;
89+
}
90+
5891
@Override
5992
public int hashCode() {
6093
return Objects.hash(certificateSecretKeySelector, enabled, privateKeySecretKeySelector);

stackgres-k8s/src/common/src/main/resources/crds/SGCluster.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ spec:
217217
Allow to enable SSL for connections to Postgres. By default is `true`.
218218
219219
If `true` certificate and private key will be auto-generated unless fields `certificateSecretKeySelector` and `privateKeySecretKeySelector` are specified.
220+
duration:
221+
type: string
222+
description: |
223+
An ISO 8601 duration in the format `PnDTnHnMn.nS`, that specifies the duration of the auto-generated certificate.
224+
225+
If not specified the default duration will be of 13 months.
226+
227+
The certificate will be re-generated 1 day or, if more, 1/12th of its duration before it expires.
220228
certificateSecretKeySelector:
221229
type: object
222230
description: |

stackgres-k8s/src/common/src/main/resources/crds/SGShardedCluster.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ spec:
223223
Allow to enable SSL for connections to Postgres. By default is `true`.
224224
225225
If `true` certificate and private key will be auto-generated unless fields `certificateSecretKeySelector` and `privateKeySecretKeySelector` are specified.
226+
duration:
227+
type: string
228+
description: |
229+
An ISO 8601 duration in the format `PnDTnHnMn.nS`, that specifies the duration of the auto-generated certificate.
230+
231+
If not specified the default duration will be of 13 months.
232+
233+
The certificate will be re-generated 1 day or, if more, 1/12th of its duration before it expires.
226234
certificateSecretKeySelector:
227235
type: object
228236
description: |

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/common/CryptoUtil.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.security.spec.InvalidKeySpecException;
3030
import java.security.spec.PKCS8EncodedKeySpec;
3131
import java.security.spec.X509EncodedKeySpec;
32+
import java.time.Duration;
3233
import java.time.Instant;
3334
import java.util.Base64;
3435
import java.util.Date;
@@ -163,7 +164,8 @@ static String getPublicKeyPem(KeyPair keyPair) throws IOException {
163164
+ END_PUBLIC_KEY + System.lineSeparator();
164165
}
165166

166-
static boolean isCertificateAndKeyValid(String certPem, String privateKeyPem) {
167+
static boolean isCertificateAndKeyValid(String certPem, String privateKeyPem,
168+
Duration gap) {
167169
try {
168170
byte[] challenge = new byte[10000];
169171
ThreadLocalRandom.current().nextBytes(challenge);
@@ -172,7 +174,7 @@ static boolean isCertificateAndKeyValid(String certPem, String privateKeyPem) {
172174
.getInstance("X509")
173175
.generateCertificate(new ByteArrayInputStream(certPem.getBytes(
174176
StandardCharsets.UTF_8)));
175-
Instant now = Instant.now();
177+
Instant now = Instant.now().plus(gap);
176178
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
177179
byte[] privateKeyEncoded = Base64.getDecoder().decode(privateKeyPem
178180
.replaceAll("-+[^-]+-+", "")

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/conciliation/factory/cluster/PostgresSslSecret.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
package io.stackgres.operator.conciliation.factory.cluster;
77

8-
import java.time.ZonedDateTime;
8+
import java.time.Duration;
9+
import java.time.Instant;
910
import java.util.HashMap;
1011
import java.util.Map;
1112
import java.util.Optional;
@@ -40,6 +41,10 @@
4041
public class PostgresSslSecret
4142
implements VolumeFactory<StackGresClusterContext> {
4243

44+
private static final Duration ONE_DAY = Duration.ofDays(1);
45+
46+
private static final long DEFAULT_DURATION = 13 * 30;
47+
4348
private static final String SSL_SUFFIX = "-ssl";
4449

4550
private final LabelFactoryForCluster labelFactory;
@@ -108,16 +113,46 @@ public PostgresSslSecret(LabelFactoryForCluster labelFactory) {
108113

109114
private void setCertificateAndPrivateKey(StackGresClusterContext context,
110115
Map<String, String> data) {
111-
var certificate = context.getPostgresSslCertificate();
112-
var privateKey = context.getPostgresSslPrivateKey();
113-
if (certificate.isEmpty() || privateKey.isEmpty()) {
114-
var certificateAndPrivateKey = CryptoUtil.generateCertificateAndPrivateKey(
115-
ZonedDateTime.now().plusYears(7500).toInstant());
116-
certificate = Optional.of(certificateAndPrivateKey.v1);
117-
privateKey = Optional.of(certificateAndPrivateKey.v2);
116+
if (Optional.ofNullable(context.getSource().getSpec())
117+
.map(StackGresClusterSpec::getPostgres)
118+
.map(StackGresClusterPostgres::getSsl)
119+
.map(StackGresClusterSsl::getPrivateKeySecretKeySelector)
120+
.isEmpty()
121+
|| Optional.ofNullable(context.getSource().getSpec())
122+
.map(StackGresClusterSpec::getPostgres)
123+
.map(StackGresClusterPostgres::getSsl)
124+
.map(StackGresClusterSsl::getCertificateSecretKeySelector)
125+
.isEmpty()) {
126+
final Duration duration = Optional.ofNullable(context.getSource().getSpec())
127+
.map(StackGresClusterSpec::getPostgres)
128+
.map(StackGresClusterPostgres::getSsl)
129+
.map(StackGresClusterSsl::getDuration)
130+
.map(Duration::parse)
131+
.orElse(Duration.ofDays(DEFAULT_DURATION));
132+
boolean certInvalid = true;
133+
if (context.getPostgresSslCertificate().isPresent()
134+
&& context.getPostgresSslPrivateKey().isPresent()) {
135+
final Duration validityGap = duration.dividedBy(12);
136+
if (CryptoUtil.isCertificateAndKeyValid(
137+
context.getPostgresSslCertificate().orElseThrow(),
138+
context.getPostgresSslPrivateKey().orElseThrow(),
139+
validityGap.compareTo(ONE_DAY) > 0 ? validityGap : ONE_DAY)) {
140+
certInvalid = false;
141+
}
142+
}
143+
144+
if (certInvalid) {
145+
var generated = CryptoUtil.generateCertificateAndPrivateKey(Instant.now().plus(duration));
146+
data.put(PatroniUtil.CERTIFICATE_KEY, generated.v1);
147+
data.put(PatroniUtil.PRIVATE_KEY_KEY, generated.v2);
148+
} else {
149+
data.put(PatroniUtil.CERTIFICATE_KEY, context.getPostgresSslCertificate().orElseThrow());
150+
data.put(PatroniUtil.PRIVATE_KEY_KEY, context.getPostgresSslPrivateKey().orElseThrow());
151+
}
152+
} else {
153+
data.put(PatroniUtil.CERTIFICATE_KEY, context.getPostgresSslCertificate().orElseThrow());
154+
data.put(PatroniUtil.PRIVATE_KEY_KEY, context.getPostgresSslPrivateKey().orElseThrow());
118155
}
119-
data.put(PatroniUtil.CERTIFICATE_KEY, certificate.orElseThrow());
120-
data.put(PatroniUtil.PRIVATE_KEY_KEY, privateKey.orElseThrow());
121156
}
122157

123158
}

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/conciliation/factory/config/OperatorSecret.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.stackgres.operator.conciliation.factory.config;
77

8+
import java.time.Duration;
89
import java.time.Instant;
910
import java.time.temporal.ChronoUnit;
1011
import java.util.HashMap;
@@ -108,7 +109,8 @@ private void setCertificate(StackGresConfigContext context, Map<String, String>
108109
certInvalid = false;
109110
} else if (CryptoUtil.isCertificateAndKeyValid(
110111
previousSecretData.get(ConfigPath.CERTIFICATE_PATH.filename()),
111-
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()))) {
112+
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()),
113+
Duration.ofDays(1))) {
112114
certInvalid = false;
113115
}
114116
}

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/conciliation/factory/config/collector/CollectorSecret.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.stackgres.operator.conciliation.factory.config.collector;
77

8+
import java.time.Duration;
89
import java.time.Instant;
910
import java.time.temporal.ChronoUnit;
1011
import java.util.HashMap;
@@ -106,7 +107,8 @@ private void setCollectorCertificate(StackGresConfigContext context, Map<String,
106107
certInvalid = false;
107108
} else if (CryptoUtil.isCertificateAndKeyValid(
108109
previousSecretData.get(ConfigPath.CERTIFICATE_PATH.filename()),
109-
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()))) {
110+
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()),
111+
Duration.ofDays(1))) {
110112
certInvalid = false;
111113
}
112114
}

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/conciliation/factory/config/webconsole/WebConsoleSecret.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.stackgres.operator.conciliation.factory.config.webconsole;
77

8+
import java.time.Duration;
89
import java.time.Instant;
910
import java.time.temporal.ChronoUnit;
1011
import java.util.HashMap;
@@ -105,7 +106,8 @@ private void setWebCertificate(StackGresConfigContext context, Map<String, Strin
105106
certInvalid = false;
106107
} else if (CryptoUtil.isCertificateAndKeyValid(
107108
previousSecretData.get(ConfigPath.CERTIFICATE_PATH.filename()),
108-
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()))) {
109+
previousSecretData.get(ConfigPath.CERTIFICATE_KEY_PATH.filename()),
110+
Duration.ofDays(1))) {
109111
certInvalid = false;
110112
}
111113
}
@@ -120,7 +122,8 @@ private void setWebCertificate(StackGresConfigContext context, Map<String, Strin
120122
rsaKeyPairInvalid = false;
121123
} else if (CryptoUtil.isCertificateAndKeyValid(
122124
previousSecretData.get("jwt-rsa.crt"),
123-
previousSecretData.get("jwt-rsa.key"))
125+
previousSecretData.get("jwt-rsa.key"),
126+
Duration.ofDays(1))
124127
&& CryptoUtil.isRsaKeyPairValid(
125128
previousSecretData.get("jwt-rsa.key"),
126129
previousSecretData.get("jwt-rsa.pub"))) {

0 commit comments

Comments
 (0)