1
1
package app .services ;
2
2
3
+ import app .config .AppProperties ;
3
4
import io .fabric8 .kubernetes .api .model .Secret ;
4
5
import io .fabric8 .kubernetes .api .model .networking .v1 .Ingress ;
5
6
import io .fabric8 .kubernetes .api .model .networking .v1 .IngressList ;
8
9
import io .fabric8 .kubernetes .client .Watch ;
9
10
import io .fabric8 .kubernetes .client .Watcher ;
10
11
import io .fabric8 .kubernetes .client .WatcherException ;
12
+ import java .io .ByteArrayInputStream ;
11
13
import java .io .Closeable ;
12
14
import java .io .IOException ;
15
+ import java .io .StringReader ;
16
+ import java .nio .charset .StandardCharsets ;
17
+ import java .security .cert .CertificateException ;
18
+ import java .security .cert .CertificateFactory ;
19
+ import java .security .cert .X509Certificate ;
20
+ import java .time .Duration ;
21
+ import java .time .Instant ;
22
+ import java .util .Base64 ;
23
+ import java .util .Base64 .Decoder ;
13
24
import java .util .Collections ;
14
25
import java .util .HashSet ;
15
26
import java .util .Map ;
16
27
import java .util .Objects ;
17
28
import java .util .Set ;
18
29
import lombok .extern .slf4j .Slf4j ;
30
+ import org .bouncycastle .util .io .pem .PemObject ;
31
+ import org .bouncycastle .util .io .pem .PemReader ;
19
32
import org .springframework .lang .NonNull ;
20
33
import org .springframework .scheduling .annotation .Scheduled ;
21
34
import org .springframework .stereotype .Service ;
@@ -26,13 +39,18 @@ public class ApplicationIngressesService implements Closeable {
26
39
27
40
private final KubernetesClient k8s ;
28
41
private final CertificateProcessingService certificateProcessingService ;
42
+ private final AppProperties appProperties ;
29
43
private final Watch ingressWatches ;
30
44
private final Watch tlsSecretWatches ;
31
45
private final Set <String /*ingress name*/ > activeReconciles = Collections .synchronizedSet (new HashSet <>());
32
46
33
- public ApplicationIngressesService (KubernetesClient k8s , CertificateProcessingService certificateProcessingService ) {
47
+ public ApplicationIngressesService (KubernetesClient k8s ,
48
+ CertificateProcessingService certificateProcessingService ,
49
+ AppProperties appProperties
50
+ ) {
34
51
this .k8s = k8s ;
35
52
this .certificateProcessingService = certificateProcessingService ;
53
+ this .appProperties = appProperties ;
36
54
37
55
this .ingressWatches = setupIngressWatch ();
38
56
this .tlsSecretWatches = setupTlsSecretWatch ();
@@ -74,7 +92,11 @@ public void onClose(WatcherException cause) {
74
92
});
75
93
}
76
94
77
- @ Scheduled (fixedDelayString = "#{@'kita-app.config.AppProperties'.certRenewalCheckInterval}" )
95
+ @ Scheduled (
96
+ // initial ingress listing will handle reconciling at startup, so delay for given interval
97
+ initialDelayString = "#{@'kita-app.config.AppProperties'.certRenewalCheckInterval}" ,
98
+ fixedDelayString = "#{@'kita-app.config.AppProperties'.certRenewalCheckInterval}"
99
+ )
78
100
public void checkCertRenewals () {
79
101
final IngressList ingresses = k8s .network ().v1 ().ingresses ()
80
102
.withLabel (Metadata .ISSUER_LABEL )
@@ -99,33 +121,80 @@ private void reconcileIngressTls(Ingress ingress) {
99
121
.withName (tls .getSecretName ())
100
122
.get ();
101
123
102
- final String requestedIssuerId = ingress .getMetadata ().getLabels ().get (Metadata .ISSUER_LABEL );
124
+ final String requestedIssuerId =
125
+ appProperties .overrideIssuer () != null ?
126
+ appProperties .overrideIssuer ()
127
+ : ingress .getMetadata ().getLabels ().get (Metadata .ISSUER_LABEL );
128
+
103
129
if (tlsSecret == null ) {
104
- initiateCertCreation (ingress , name , tls , requestedIssuerId );
130
+ initiateCertCreation (ingress , tls , requestedIssuerId );
105
131
} else {
106
132
final String tlsSecretIssuer = nullSafe (tlsSecret .getMetadata ().getLabels ()).get (Metadata .ISSUER_LABEL );
107
- if (!Objects .equals (tlsSecretIssuer , requestedIssuerId )) {
108
- initiateCertCreation (ingress , name , tls , requestedIssuerId );
133
+ if (!Objects .equals (tlsSecretIssuer , requestedIssuerId )
134
+ || needsRenewal (tlsSecret )) {
135
+ initiateCertCreation (ingress , tls , requestedIssuerId );
109
136
} else {
110
- // TODO is cert needing refresh
111
137
activeReconciles .remove (name );
112
138
}
113
139
}
114
140
}
115
141
116
142
}
117
143
118
- private void initiateCertCreation (Ingress ingress , String name , IngressTLS tls , String requestedIssuerId ) {
144
+ private boolean needsRenewal (Secret tlsSecret ) {
145
+ final String certContentEncoded = tlsSecret .getData ().get ("tls.crt" );
146
+ if (certContentEncoded != null ) {
147
+ final Decoder decoder = Base64 .getDecoder ();
148
+
149
+ try (PemReader pemReader = new PemReader (new StringReader (
150
+ new String (decoder .decode (certContentEncoded ), StandardCharsets .UTF_8 )
151
+ ))) {
152
+ final PemObject pemObject = pemReader .readPemObject ();
153
+
154
+ CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
155
+ final X509Certificate cert = (X509Certificate ) cf .generateCertificate (
156
+ new ByteArrayInputStream (pemObject .getContent ()));
157
+ final Instant notAfter = cert .getNotAfter ().toInstant ();
158
+ final Instant notBefore = cert .getNotBefore ().toInstant ();
159
+ final Duration lifetime = Duration .between (notBefore ,
160
+ // since it sets expiration just before and between's argument is exclusive
161
+ notAfter .plusSeconds (1 )
162
+ );
163
+ // LetsEncrypt recommends renewing when there is a 3rd of lifetime left
164
+ // https://letsencrypt.org/docs/integration-guide/#when-to-renew
165
+ if (Instant .now ().isAfter (notAfter .minus (lifetime .dividedBy (3 )))) {
166
+ log .info ("TLS secret {} is due to be renewed since its lifetime is {} days and expires at {}" ,
167
+ tlsSecret .getMetadata ().getName (), lifetime .toDays (), notAfter
168
+ );
169
+ return true ;
170
+ }
171
+ } catch (IOException e ) {
172
+ log .error ("Failed to read/close PEM reader" , e );
173
+ } catch (CertificateException e ) {
174
+ log .error ("Failed to get X.509 cert factory" , e );
175
+ }
176
+ } else {
177
+ log .error ("TLS secret {} is missing tls.crt data" , tlsSecret .getMetadata ().getName ());
178
+ }
179
+ return false ;
180
+ }
181
+
182
+ private void initiateCertCreation (Ingress ingress , IngressTLS tls , String requestedIssuerId ) {
183
+ final String ingressName = ingress .getMetadata ().getName ();
184
+ if (appProperties .dryRun ()) {
185
+ log .info ("Skipping cert creation of {} for ingress {} since dry-run is enabled" ,
186
+ tls .getSecretName (), ingressName
187
+ );
188
+ return ;
189
+ }
190
+
119
191
certificateProcessingService .initiateCertCreation (ingress , tls , requestedIssuerId )
120
- .subscribe (secret -> {
192
+ .subscribe (secret ->
121
193
log .info ("Cert creation complete for tls entry with secret={} hosts={} in ingress={}" ,
122
- secret .getMetadata ().getName (), tls .getHosts (), name
123
- );
124
- },
125
- throwable -> {
126
- log .warn ("Problem while processing cert creation" );
127
- },
128
- () -> activeReconciles .remove (name )
194
+ secret .getMetadata ().getName (), tls .getHosts (), ingressName
195
+ ),
196
+ throwable -> log .warn ("Problem while processing cert creation" ),
197
+ () -> activeReconciles .remove (ingressName )
129
198
);
130
199
}
131
200
@@ -135,7 +204,7 @@ private Map<String, String> nullSafe(Map<String, String> value) {
135
204
}
136
205
137
206
@ Override
138
- public void close () throws IOException {
207
+ public void close () {
139
208
ingressWatches .close ();
140
209
tlsSecretWatches .close ();
141
210
}
0 commit comments