diff --git a/headlamp-sealed-secrets/src/components/EncryptDialog.tsx b/headlamp-sealed-secrets/src/components/EncryptDialog.tsx
index aa13e0c..87267c7 100644
--- a/headlamp-sealed-secrets/src/components/EncryptDialog.tsx
+++ b/headlamp-sealed-secrets/src/components/EncryptDialog.tsx
@@ -25,7 +25,12 @@ import {
import { useSnackbar } from 'notistack';
import React from 'react';
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
-import { encryptKeyValues, parsePublicKeyFromCert } from '../lib/crypto';
+import {
+ encryptKeyValues,
+ isCertificateExpiringSoon,
+ parseCertificateInfo,
+ parsePublicKeyFromCert,
+} from '../lib/crypto';
import { SealedSecret } from '../lib/SealedSecretCRD';
import { validateSecretKey, validateSecretName, validateSecretValue } from '../lib/validators';
import { PlaintextValue, SealedSecretScope, SecretKeyValue } from '../types';
@@ -125,7 +130,27 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
return;
}
- // 2. Parse the public key
+ // 2. Check certificate expiry
+ const certInfoResult = parseCertificateInfo(certResult.value);
+ if (certInfoResult.ok) {
+ const certInfo = certInfoResult.value;
+
+ if (certInfo.isExpired) {
+ enqueueSnackbar(
+ `Warning: Controller certificate expired on ${certInfo.validTo.toLocaleDateString()}. ` +
+ 'Secrets may not be decryptable.',
+ { variant: 'warning' }
+ );
+ } else if (isCertificateExpiringSoon(certInfo, 30)) {
+ enqueueSnackbar(
+ `Warning: Controller certificate expires in ${certInfo.daysUntilExpiry} days ` +
+ `(${certInfo.validTo.toLocaleDateString()}).`,
+ { variant: 'warning' }
+ );
+ }
+ }
+
+ // 3. Parse the public key
const keyResult = parsePublicKeyFromCert(certResult.value);
if (keyResult.ok === false) {
@@ -133,7 +158,7 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
return;
}
- // 3. Encrypt all values client-side
+ // 4. Encrypt all values client-side
const encryptResult = encryptKeyValues(
keyResult.value,
validKeyValues.map(kv => ({ key: kv.key, value: PlaintextValue(kv.value) })),
@@ -147,7 +172,7 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
return;
}
- // 4. Construct the SealedSecret object
+ // 5. Construct the SealedSecret object
const sealedSecretData: any = {
apiVersion: 'bitnami.com/v1alpha1',
kind: 'SealedSecret',
@@ -171,7 +196,7 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
sealedSecretData.metadata.annotations['sealedsecrets.bitnami.com/cluster-wide'] = 'true';
}
- // 5. Apply to the cluster
+ // 6. Apply to the cluster
await SealedSecret.apiEndpoint.post(sealedSecretData);
enqueueSnackbar('SealedSecret created successfully', { variant: 'success' });
diff --git a/headlamp-sealed-secrets/src/components/SealingKeysView.tsx b/headlamp-sealed-secrets/src/components/SealingKeysView.tsx
index ca04138..b15442e 100644
--- a/headlamp-sealed-secrets/src/components/SealingKeysView.tsx
+++ b/headlamp-sealed-secrets/src/components/SealingKeysView.tsx
@@ -6,34 +6,18 @@
import { K8s } from '@kinvolk/headlamp-plugin/lib';
import { SectionBox, SimpleTable, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
-import { Box, Button } from '@mui/material';
-import forge from 'node-forge';
+import { Box, Button, Chip } from '@mui/material';
import { useSnackbar } from 'notistack';
import React from 'react';
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
-import { PEMCertificate } from '../types';
+import { isCertificateExpiringSoon, parseCertificateInfo } from '../lib/crypto';
+import { CertificateInfo, PEMCertificate } from '../types';
interface SealingKey {
name: string;
status: 'active' | 'compromised';
created: string;
- notBefore?: string;
- notAfter?: string;
-}
-
-/**
- * Parse certificate dates from TLS secret
- */
-function parseCertificateDates(certPem: PEMCertificate): { notBefore?: string; notAfter?: string } {
- try {
- const cert = forge.pki.certificateFromPem(certPem);
- return {
- notBefore: cert.validity.notBefore.toISOString(),
- notAfter: cert.validity.notAfter.toISOString(),
- };
- } catch {
- return {};
- }
+ certInfo?: CertificateInfo;
}
/**
@@ -58,13 +42,20 @@ export function SealingKeysView() {
| 'active'
| 'compromised';
const certPem = secret.data?.['tls.crt'] ? atob(secret.data['tls.crt']) : '';
- const dates = certPem ? parseCertificateDates(PEMCertificate(certPem)) : {};
+
+ let certInfo: CertificateInfo | undefined;
+ if (certPem) {
+ const infoResult = parseCertificateInfo(PEMCertificate(certPem));
+ if (infoResult.ok) {
+ certInfo = infoResult.value;
+ }
+ }
return {
name: secret.metadata.name!,
status,
created: secret.metadata.creationTimestamp!,
- ...dates,
+ certInfo,
};
})
.sort((a, b) => {
@@ -148,14 +139,44 @@ export function SealingKeysView() {
getter: (key: SealingKey) => new Date(key.created).toLocaleString(),
},
{
- label: 'Valid From',
- getter: (key: SealingKey) =>
- key.notBefore ? new Date(key.notBefore).toLocaleString() : 'N/A',
- },
- {
- label: 'Valid Until',
- getter: (key: SealingKey) =>
- key.notAfter ? new Date(key.notAfter).toLocaleString() : 'N/A',
+ label: 'Certificate Expiry',
+ getter: (key: SealingKey) => {
+ if (!key.certInfo) return 'N/A';
+
+ const { certInfo } = key;
+ const expiryDate = certInfo.validTo.toLocaleDateString();
+
+ if (certInfo.isExpired) {
+ return (
+
+
+ {expiryDate}
+
+ );
+ }
+
+ if (isCertificateExpiringSoon(certInfo, 30)) {
+ return (
+
+
+ {expiryDate}
+
+ );
+ }
+
+ return (
+
+ {expiryDate}
+
+ ({certInfo.daysUntilExpiry} days)
+
+
+ );
+ },
},
]}
/>
diff --git a/headlamp-sealed-secrets/src/lib/crypto.ts b/headlamp-sealed-secrets/src/lib/crypto.ts
index 4b4f3a7..30618de 100644
--- a/headlamp-sealed-secrets/src/lib/crypto.ts
+++ b/headlamp-sealed-secrets/src/lib/crypto.ts
@@ -14,6 +14,7 @@
import forge from 'node-forge';
import {
Base64String,
+ CertificateInfo,
Err,
Ok,
PEMCertificate,
@@ -154,3 +155,68 @@ export function validateCertificate(pemCert: PEMCertificate): boolean {
const result = parsePublicKeyFromCert(pemCert);
return result.ok;
}
+
+/**
+ * Parse certificate and extract metadata
+ *
+ * Extracts validity dates, issuer/subject information, and calculates
+ * expiration status and fingerprint.
+ *
+ * @param pemCert PEM-encoded certificate string (branded type)
+ * @returns Result containing certificate information or error message
+ */
+export function parseCertificateInfo(pemCert: PEMCertificate): Result {
+ try {
+ const cert = forge.pki.certificateFromPem(pemCert);
+ const now = new Date();
+
+ // Extract validity dates
+ const validFrom = cert.validity.notBefore;
+ const validTo = cert.validity.notAfter;
+
+ // Calculate expiration status
+ const isExpired = now > validTo;
+ const daysUntilExpiry = Math.floor((validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+
+ // Format issuer and subject
+ const formatDN = (attributes: forge.pki.CertificateField[]): string => {
+ return attributes.map(a => `${a.shortName}=${a.value}`).join(', ');
+ };
+
+ const issuer = formatDN(cert.issuer.attributes);
+ const subject = formatDN(cert.subject.attributes);
+
+ // Calculate SHA-256 fingerprint
+ const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
+ const md = forge.md.sha256.create();
+ md.update(der);
+ const fingerprint = md.digest().toHex().toUpperCase();
+
+ // Get serial number
+ const serialNumber = cert.serialNumber;
+
+ return Ok({
+ validFrom,
+ validTo,
+ isExpired,
+ daysUntilExpiry,
+ issuer,
+ subject,
+ fingerprint,
+ serialNumber,
+ });
+ } catch (error) {
+ return Err(`Failed to parse certificate info: ${error}`);
+ }
+}
+
+/**
+ * Check if certificate will expire soon (within threshold)
+ *
+ * @param info Certificate information
+ * @param daysThreshold Number of days to consider "expiring soon" (default: 30)
+ * @returns true if certificate will expire within threshold days
+ */
+export function isCertificateExpiringSoon(info: CertificateInfo, daysThreshold = 30): boolean {
+ return !info.isExpired && info.daysUntilExpiry <= daysThreshold;
+}
diff --git a/headlamp-sealed-secrets/src/types.ts b/headlamp-sealed-secrets/src/types.ts
index 731f088..f1e6978 100644
--- a/headlamp-sealed-secrets/src/types.ts
+++ b/headlamp-sealed-secrets/src/types.ts
@@ -259,3 +259,25 @@ export interface EncryptionRequest {
scope: SealedSecretScope;
keyValues: SecretKeyValue[];
}
+
+/**
+ * Certificate information extracted from PEM certificate
+ */
+export interface CertificateInfo {
+ /** Validity period start date */
+ validFrom: Date;
+ /** Validity period end date */
+ validTo: Date;
+ /** Whether certificate is currently expired */
+ isExpired: boolean;
+ /** Days until expiry (negative if expired) */
+ daysUntilExpiry: number;
+ /** Certificate issuer (formatted as DN string) */
+ issuer: string;
+ /** Certificate subject (formatted as DN string) */
+ subject: string;
+ /** SHA-256 fingerprint of certificate */
+ fingerprint: string;
+ /** Serial number of certificate */
+ serialNumber: string;
+}