mgr/dashboard : Enable cephadm-signed TLS for NVMeOf

fixes: https://tracker.ceph.com/issues/77406
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>

(cherry picked from commit 9623f6847cecf7f2e79f461420fce4821fd1b159)
This commit is contained in:
Abhishek Desai 2026-06-15 16:42:15 +05:30 committed by pujashahu
parent 440cba650a
commit 517f6da8a9
3 changed files with 192 additions and 123 deletions

View File

@ -1153,112 +1153,8 @@
}
<!-- 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 clusters 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 -->
@ -1396,6 +1292,9 @@
</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>
@ -1452,6 +1351,7 @@
</div>
}
}
}
</form>
</section>
<cd-form-button-panel (submitActionEvent)="onSubmit()"
@ -1467,6 +1367,115 @@
</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 clusters 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"

View File

@ -558,6 +558,22 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
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({
@ -572,6 +588,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
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');
@ -585,6 +602,8 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
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',
@ -592,6 +611,23 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
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', () => {

View File

@ -238,7 +238,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
CdValidators.composeIf(
{
service_type: 'nvmeof',
enable_mtls: true
enable_mtls: true,
certificateType: CertificateType.external
},
[Validators.required]
)
@ -250,7 +251,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
CdValidators.composeIf(
{
service_type: 'nvmeof',
enable_mtls: true
enable_mtls: true,
certificateType: CertificateType.external
},
[Validators.required]
)
@ -262,7 +264,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
CdValidators.composeIf(
{
service_type: 'nvmeof',
enable_mtls: true
enable_mtls: true,
certificateType: CertificateType.external
},
[Validators.required]
)
@ -274,7 +277,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
CdValidators.composeIf(
{
service_type: 'nvmeof',
enable_mtls: true
enable_mtls: true,
certificateType: CertificateType.external
},
[Validators.required]
)
@ -286,7 +290,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
CdValidators.composeIf(
{
service_type: 'nvmeof',
enable_mtls: true
enable_mtls: true,
certificateType: CertificateType.external
},
[Validators.required]
)
@ -781,11 +786,19 @@ export class ServiceFormComponent extends CdForm implements OnInit {
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
@ -1329,11 +1342,22 @@ export class ServiceFormComponent extends CdForm implements OnInit {
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':
@ -1583,7 +1607,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
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);
}
@ -1593,7 +1617,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
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);
}