}
<!-- Certificate Management UI -->
- @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nvmeof', 'nfs'].includes(serviceForm.controls.service_type.value)) {
- <div>
- <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
- @if (editing && currentCertificate?.has_certificate) {
- <div class="form-item">
- <label class="cds--label fw-bold"
- i18n>Current Certificate</label>
- <div class="row">
- <div class="col-6">
- <label class="cds--label"
- i18n>Certificate</label>
- <div>{{ currentCertificate.cert_name }}</div>
- </div>
- <div class="col-6">
- <label class="cds--label"
- i18n>Valid Until</label>
- <div>{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} <span i18n>days left</span></div>
- </div>
- </div>
- <div class="row mt-3">
- <div class="col-6">
- <label class="cds--label"
- i18n>Status</label>
- <div class="align-items-center">
- <cd-icon [type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"></cd-icon>
- @switch (currentCertificate.status) {
- @case ('valid') { <span i18n>Valid</span> }
- @case ('expiring') { <span i18n>Expiring soon</span> }
- @case ('expired') { <span i18n>Expired</span> }
- @case ('not_configured') { <span i18n>Not configured</span> }
- @case ('invalid') { <span i18n>Invalid</span> }
- @default { {{ currentCertificate.status }} }
- }
- </div>
- </div>
- <div class="col-6">
- <label class="cds--label"
- i18n>Issuer</label>
- <div>
- @if (currentCertificate.signed_by === 'cephadm') {
- <span i18n>Internal (Cephadm CA)</span>
- } @else {
- {{ currentCertificate.issuer || 'External' }}
- }
- </div>
- </div>
- </div>
- </div>
- }
-
- <!-- Certificate Authority Selection -->
- <div class="form-item">
- <label class="cds--label fw-bold"
- i18n>Choose Certificate Authority</label>
- <cds-radio-group formControlName="certificateType"
- orientation="horizontal"
- helperText="Select how certificates will be signed for this service. Choose internal to use the cluster’s CA, or external to upload certificates signed by your organization.">
- <cds-radio [value]="CertificateType.internal"
- (change)="onCertificateTypeChange(CertificateType.internal)"
- i18n>
- Internal
- </cds-radio>
- <cds-radio [value]="CertificateType.external"
- (change)="onCertificateTypeChange(CertificateType.external)"
- i18n>
- External
- </cds-radio>
- </cds-radio-group>
- </div>
-
- @if (showCertSourceChangeWarning) {
- <cd-alert-panel type="warning"
- spacingClass="mb-3"
- i18n>
- Changing the certificate source will redeploy the service daemons to apply the new certificate configuration.
- </cd-alert-panel>
- }
-
- @if (serviceForm.controls.certificateType.value === CertificateType.internal) {
- <cd-alert-panel type="info"
- spacingClass="mb-3"
- i18n>
- Certificate will be generated automatically by Cephadm CA for internal certificate type.
- </cd-alert-panel>
- <div class="form-item">
- <cd-text-label-list formControlName="custom_sans"
- label="Custom SAN Entries"
- i18n-label
- helperText="Optional list of Subject Alternative Names (hostnames, IPs, or DNS names) to include in the auto-generated certificate."
- i18n-helperText>
- </cd-text-label-list>
- </div>
- @if (serviceForm.controls.service_type.value === 'rgw'
- && serviceForm.controls.virtual_host_enabled.value) {
- <div class="form-item">
- <cds-checkbox i18n-label
- formControlName="wildcard_enabled">
- Include wildcard certificate for bucket subdomains
- <cd-help-text i18n>
- Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for virtual-host style with SSL.
- </cd-help-text>
- </cds-checkbox>
- </div>
- }
- }
- </div>
+ @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nfs'].includes(serviceForm.controls.service_type.value)) {
+ <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
}
<!-- ssl_cert - Only show when SSL is enabled AND certificate type is external -->
</fieldset>
</div>
@if (serviceForm.controls.enable_mtls.value) {
+ <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
+
+ @if (serviceForm.controls.certificateType.value === CertificateType.external) {
<!-- root_ca_cert -->
<div class="form-item">
<ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'root_ca_cert', title: 'Root CA Certificate Input', helperText: 'Uploaded files will populate the Root CA certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a Root CA certificate file, or paste the Root CA certificate PEM content directly.', invalidTemplate: invalidRootCaCertError, isRequired: true }"></ng-container>
</div>
}
}
+ }
</form>
</section>
<cd-form-button-panel (submitActionEvent)="onSubmit()"
</span>
</ng-template>
+<ng-template #certificateAuthorityManagement>
+ <div [formGroup]="serviceForm">
+ <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
+ @if (editing && currentCertificate?.has_certificate) {
+ <div class="form-item">
+ <label class="cds--label cds--type-heading-compact-01"
+ i18n>Current Certificate</label>
+ <div class="row">
+ <div class="col-6">
+ <label class="cds--label"
+ i18n>Certificate</label>
+ <div>{{ currentCertificate.cert_name }}</div>
+ </div>
+ <div class="col-6">
+ <label class="cds--label"
+ i18n>Valid Until</label>
+ <div>{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} <span i18n>days left</span></div>
+ </div>
+ </div>
+ <div class="row mt-3">
+ <div class="col-6">
+ <label class="cds--label"
+ i18n>Status</label>
+ <div class="align-items-center">
+ <cd-icon [type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"></cd-icon>
+ @switch (currentCertificate.status) {
+ @case ('valid') { <span i18n>Valid</span> }
+ @case ('expiring') { <span i18n>Expiring soon</span> }
+ @case ('expired') { <span i18n>Expired</span> }
+ @case ('not_configured') { <span i18n>Not configured</span> }
+ @case ('invalid') { <span i18n>Invalid</span> }
+ @default { {{ currentCertificate.status }} }
+ }
+ </div>
+ </div>
+ <div class="col-6">
+ <label class="cds--label"
+ i18n>Issuer</label>
+ <div>
+ @if (currentCertificate.signed_by === 'cephadm') {
+ <span i18n>Internal (Cephadm CA)</span>
+ } @else {
+ {{ currentCertificate.issuer || 'External' }}
+ }
+ </div>
+ </div>
+ </div>
+ </div>
+ }
+
+ <!-- Certificate Authority Selection -->
+ <div class="form-item">
+ <label class="cds--label cds--type-heading-compact-01"
+ i18n>Choose Certificate Authority</label>
+ <cds-radio-group formControlName="certificateType"
+ orientation="horizontal"
+ helperText="Select how certificates will be signed for this service. Choose internal to use the cluster’s CA, or external to upload certificates signed by your organization."
+ i18n-helperText>
+ <cds-radio [value]="CertificateType.internal"
+ (change)="onCertificateTypeChange(CertificateType.internal)"
+ i18n>
+ Internal
+ </cds-radio>
+ <cds-radio [value]="CertificateType.external"
+ (change)="onCertificateTypeChange(CertificateType.external)"
+ i18n>
+ External
+ </cds-radio>
+ </cds-radio-group>
+ </div>
+
+ @if (showCertSourceChangeWarning) {
+ <cd-alert-panel type="warning"
+ spacingClass="mb-3"
+ i18n>
+ Changing the certificate source will redeploy the service daemons to apply the new certificate configuration.
+ </cd-alert-panel>
+ }
+
+ @if (serviceForm.controls.certificateType.value === CertificateType.internal) {
+ <cd-alert-panel type="info"
+ spacingClass="mb-3"
+ i18n>
+ Certificate will be generated automatically by Cephadm CA for internal certificate type.
+ </cd-alert-panel>
+ <div class="form-item">
+ <cd-text-label-list formControlName="custom_sans"
+ label="Custom SAN Entries"
+ i18n-label
+ helperText="Optional list of Subject Alternative Names (hostnames, IPs, or DNS names) to include in the auto-generated certificate."
+ i18n-helperText>
+ </cd-text-label-list>
+ </div>
+ @if (serviceForm.controls.service_type.value === 'rgw'
+ && serviceForm.controls.virtual_host_enabled.value) {
+ <div class="form-item">
+ <cds-checkbox i18n-label
+ formControlName="wildcard_enabled">
+ Include wildcard certificate for bucket subdomains
+ <cd-help-text i18n>
+ Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for virtual-host style with SSL.
+ </cd-help-text>
+ </cds-checkbox>
+ </div>
+ }
+ }
+ </div>
+</ng-template>
+
<ng-template #fileUploaderTextarea
let-controlName="controlName"
let-title="title"
expect(server_key).toBeNull();
});
+ it('should not show certs and keys field with internal mTLS', () => {
+ formHelper.setValue('enable_mtls', true);
+ formHelper.setValue('certificateType', 'internal');
+ fixture.detectChanges();
+ const root_ca_cert = fixture.debugElement.query(By.css('#root_ca_cert'));
+ const client_cert = fixture.debugElement.query(By.css('#client_cert'));
+ const client_key = fixture.debugElement.query(By.css('#client_key'));
+ const server_cert = fixture.debugElement.query(By.css('#server_cert'));
+ const server_key = fixture.debugElement.query(By.css('#server_key'));
+ expect(root_ca_cert).toBeNull();
+ expect(client_cert).toBeNull();
+ expect(client_key).toBeNull();
+ expect(server_cert).toBeNull();
+ expect(server_key).toBeNull();
+ });
+
it('should submit nvmeof without mTLS', () => {
component.onSubmit();
expect(cephServiceService.create).toHaveBeenCalledWith({
it('should submit nvmeof with mTLS', () => {
formHelper.setValue('enable_mtls', true);
+ formHelper.setValue('certificateType', 'external');
formHelper.setValue('root_ca_cert', 'root_ca_cert');
formHelper.setValue('client_cert', 'client_cert');
formHelper.setValue('client_key', 'client_key');
unmanaged: false,
group: 'default',
enable_auth: true,
+ ssl: true,
+ certificate_source: 'inline',
root_ca_cert: 'root_ca_cert',
client_cert: 'client_cert',
client_key: 'client_key',
server_key: 'server_key'
});
});
+
+ it('should submit nvmeof with internal mTLS', () => {
+ formHelper.setValue('enable_mtls', true);
+ formHelper.setValue('certificateType', 'internal');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'nvmeof',
+ service_id: 'rbd.default',
+ placement: {},
+ unmanaged: false,
+ pool: 'rbd',
+ group: 'default',
+ enable_auth: true,
+ ssl: true,
+ certificate_source: 'cephadm-signed'
+ });
+ });
});
describe('should test service smb', () => {
CdValidators.composeIf(
{
service_type: 'nvmeof',
- enable_mtls: true
+ enable_mtls: true,
+ certificateType: CertificateType.external
},
[Validators.required]
)
CdValidators.composeIf(
{
service_type: 'nvmeof',
- enable_mtls: true
+ enable_mtls: true,
+ certificateType: CertificateType.external
},
[Validators.required]
)
CdValidators.composeIf(
{
service_type: 'nvmeof',
- enable_mtls: true
+ enable_mtls: true,
+ certificateType: CertificateType.external
},
[Validators.required]
)
CdValidators.composeIf(
{
service_type: 'nvmeof',
- enable_mtls: true
+ enable_mtls: true,
+ certificateType: CertificateType.external
},
[Validators.required]
)
CdValidators.composeIf(
{
service_type: 'nvmeof',
- enable_mtls: true
+ enable_mtls: true,
+ certificateType: CertificateType.external
},
[Validators.required]
)
case 'nvmeof':
this.serviceForm.get('group').setValue(response[0].spec.group);
this.serviceForm.get('enable_mtls').setValue(response[0].spec?.enable_auth);
- this.serviceForm.get('root_ca_cert').setValue(response[0].spec?.root_ca_cert);
- this.serviceForm.get('client_cert').setValue(response[0].spec?.client_cert);
- this.serviceForm.get('client_key').setValue(response[0].spec?.client_key);
- this.serviceForm.get('server_cert').setValue(response[0].spec?.server_cert);
- this.serviceForm.get('server_key').setValue(response[0].spec?.server_key);
+ if (response[0].spec?.enable_auth) {
+ if (response[0].spec?.certificate_source !== 'cephadm-signed') {
+ this.serviceForm.get('certificateType').setValue(CertificateType.external);
+ }
+ if (response[0].spec?.['custom_sans']) {
+ this.serviceForm.get('custom_sans').setValue(response[0].spec['custom_sans']);
+ }
+ this.serviceForm.get('root_ca_cert').setValue(response[0].spec?.root_ca_cert);
+ this.serviceForm.get('client_cert').setValue(response[0].spec?.client_cert);
+ this.serviceForm.get('client_key').setValue(response[0].spec?.client_key);
+ this.serviceForm.get('server_cert').setValue(response[0].spec?.server_cert);
+ this.serviceForm.get('server_key').setValue(response[0].spec?.server_key);
+ }
break;
case 'rgw':
this.serviceForm
serviceSpec['group'] = values['group'];
serviceSpec['enable_auth'] = values['enable_mtls'];
if (values['enable_mtls']) {
- serviceSpec['root_ca_cert'] = values['root_ca_cert'];
- serviceSpec['client_cert'] = values['client_cert'];
- serviceSpec['client_key'] = values['client_key'];
- serviceSpec['server_cert'] = values['server_cert'];
- serviceSpec['server_key'] = values['server_key'];
+ serviceSpec['ssl'] = true;
+ serviceSpec['certificate_source'] =
+ values['certificateType'] === CertificateType.internal ? 'cephadm-signed' : 'inline';
+ if (
+ values['certificateType'] === CertificateType.internal &&
+ values['custom_sans']?.length > 0
+ ) {
+ serviceSpec['custom_sans'] = values['custom_sans'];
+ }
+ if (values['certificateType'] === CertificateType.external) {
+ serviceSpec['root_ca_cert'] = values['root_ca_cert'];
+ serviceSpec['client_cert'] = values['client_cert'];
+ serviceSpec['client_key'] = values['client_key'];
+ serviceSpec['server_cert'] = values['server_cert'];
+ serviceSpec['server_key'] = values['server_key'];
+ }
}
break;
case 'iscsi':
return isExternalCert;
}
- const sslCertServices = ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs'];
+ const sslCertServices = ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'nfs'];
return isSslEnabled && isExternalCert && sslCertServices.includes(serviceType);
}
this.serviceForm.controls.certificateType?.value === CertificateType.external;
const isSslEnabled = this.serviceForm.controls.ssl?.value;
- const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs', 'mgmt-gateway'];
+ const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nfs', 'mgmt-gateway'];
return isSslEnabled && isExternalCert && sslKeyServices.includes(serviceType);
}