Line data Source code
1 : /*
2 : * Copyright (C) 2022 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 92 : import cockpit from "cockpit";
7 92 : import React, { useState } from 'react';
8 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
9 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
10 : import {
11 : Modal, ModalBody, ModalFooter, ModalHeader
12 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
13 : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
14 : import { ExclamationTriangleIcon, ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
15 :
16 : import { ModalError } from 'cockpit-components-inline-notification.jsx';
17 : import { PrivilegedButton } from "cockpit-components-privileged";
18 : import { ProfilesMenuDialogBody } from "./profiles-menu-dialog-body";
19 : import { useDialogs } from "dialogs.jsx";
20 : import { useInit } from "hooks";
21 :
22 : import "./cryptoPolicies.scss";
23 :
24 92 : const _ = cockpit.gettext;
25 :
26 11 : const displayProfileText = profile => profile === "DEFAULT" ? _("Default") : profile;
27 84 : const isInconsistentPolicy = (policy, fipsEnabled) => policy === "FIPS" !== fipsEnabled;
28 :
29 92 : const getFipsConfigurable = () => cockpit.spawn(["/bin/sh", "-c", "command -v fips-mode-setup"], { error: "ignore" })
30 0 : .then(() => true)
31 86 : .catch(() => false);
32 :
33 92 : export const CryptoPolicyRow = () => {
34 92 : const Dialogs = useDialogs();
35 92 : const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
36 92 : const [fipsEnabled, setFipsEnabled] = useState(null);
37 92 : const [fipsConfigurable, setFipsConfigurable] = useState(null);
38 92 : const [shaSubPolicyAvailable, setShaSubPolicyAvailable] = useState(null);
39 :
40 92 : useInit(() => {
41 92 : cockpit.file("/proc/sys/crypto/fips_enabled").read()
42 16 : .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
43 85 : getFipsConfigurable().then(v => setFipsConfigurable(v));
44 92 : cockpit.file("/etc/crypto-policies/config")
45 86 : .watch(async contents => {
46 : // Ask crypto-policies to get correct FIPS state, as that dominates the configured policy
47 86 : try {
48 75 : setCurrentCryptoPolicy((await cockpit.spawn(["update-crypto-policies", "--show"])).trim());
49 16 : } catch (error) {
50 16 : console.warn("Failed to get current crypto policy:", error.toString(),
51 16 : "; falling back to /etc/crypto-policies/config");
52 0 : const filteredContent = contents?.split('\n').filter(line => !line.startsWith("#")).join('\n');
53 16 : setCurrentCryptoPolicy(filteredContent?.trim() ?? null);
54 16 : }
55 86 : });
56 : // RHEL-8-8 has no SHA1 subpolicy
57 92 : cockpit.file("/usr/share/crypto-policies/policies/modules/SHA1.pmod").read()
58 16 : .then(content => setShaSubPolicyAvailable(content ? content.trim() : false));
59 92 : });
60 :
61 75 : if (currentCryptoPolicy === null || fipsEnabled === null || fipsConfigurable === null)
62 92 : return null;
63 :
64 16 : const policyRender = (currentCryptoPolicy.startsWith("FIPS") && !fipsConfigurable)
65 : /* read-only mode; can't switch away from FIPS without fips-mode-setup */
66 16 : ? <span id="crypto-policy-current">{displayProfileText(currentCryptoPolicy)}</span>
67 75 : : <PrivilegedButton variant="link" buttonId="crypto-policy-button" tooltipId="tip-crypto-policy"
68 75 : excuse={ _("The user $0 is not permitted to change cryptographic policies") }
69 1 : onClick={() => Dialogs.show(<CryptoPolicyDialog
70 1 : currentCryptoPolicy={currentCryptoPolicy}
71 1 : setCurrentCryptoPolicy={setCurrentCryptoPolicy}
72 1 : fipsEnabled={fipsEnabled}
73 1 : fipsConfigurable={fipsConfigurable}
74 1 : shaSubPolicyAvailable={shaSubPolicyAvailable} />)}>
75 75 : {displayProfileText(currentCryptoPolicy)}
76 75 : </PrivilegedButton>;
77 :
78 92 : return (
79 92 : <tr className="pf-v6-c-table__tr">
80 92 : <th className="pf-v6-c-table__th" scope="row">{_("Cryptographic policy")}</th>
81 92 : <td className="pf-v6-c-table__td">{policyRender}</td>
82 92 : </tr>
83 : );
84 92 : };
85 :
86 0 : const setPolicy = async (policy, setError, setInProgress, fipsConfigurable) => {
87 0 : setInProgress(true);
88 :
89 0 : try {
90 0 : if (policy === "FIPS") {
91 0 : cockpit.assert(fipsConfigurable, "calling setPolicy(FIPS) without fips-mode-setup");
92 0 : await cockpit.spawn(["fips-mode-setup", "--enable"], { superuser: "require", err: "message" });
93 0 : } else {
94 0 : if (fipsConfigurable)
95 0 : await cockpit.spawn(["fips-mode-setup", "--disable"], { superuser: "require", err: "message" });
96 0 : await cockpit.spawn(["update-crypto-policies", "--set", policy], { superuser: "require", err: "message" });
97 0 : }
98 :
99 0 : await cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" });
100 0 : } catch (error) {
101 0 : setError(error);
102 0 : } finally {
103 0 : setInProgress(false);
104 0 : }
105 0 : };
106 :
107 1 : const CryptoPolicyDialog = ({
108 1 : currentCryptoPolicy,
109 1 : fipsEnabled,
110 1 : fipsConfigurable,
111 1 : reApply,
112 1 : shaSubPolicyAvailable,
113 1 : }) => {
114 1 : const Dialogs = useDialogs();
115 1 : const [error, setError] = useState();
116 1 : const [inProgress, setInProgress] = useState(false);
117 1 : const [selected, setSelected] = useState(currentCryptoPolicy);
118 :
119 : // Found in /usr/share/crypto-policies/policies/
120 1 : const cryptopolicies = {
121 1 : DEFAULT: _("Recommended, secure settings for current threat models."),
122 1 : "DEFAULT:SHA1": _("DEFAULT with SHA-1 signature verification allowed."),
123 1 : LEGACY: _("Higher interoperability at the cost of an increased attack surface."),
124 1 : "LEGACY:AD-SUPPORT": _("LEGACY with Active Directory interoperability."),
125 1 : FIPS: (<Flex alignItems={{ default: 'alignItemsCenter' }}>
126 1 : {_("Only use approved and allowed algorithms when booting in FIPS mode.")}
127 1 : <Button component='a'
128 1 : rel="noopener noreferrer" target="_blank"
129 1 : variant='link'
130 1 : isInline
131 1 : icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
132 1 : href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
133 1 : {_("Learn more")}
134 1 : </Button>
135 1 : </Flex>),
136 1 : "FIPS:OSPP": _("FIPS with further Common Criteria restrictions."),
137 1 : FUTURE: _("Protects from anticipated near-term future attacks at the expense of interoperability."),
138 1 : };
139 :
140 1 : const policies = Object.keys(cryptopolicies)
141 1 : .filter(pol => pol.endsWith(':SHA1') ? shaSubPolicyAvailable : true)
142 : // cannot enable fips without fips-mode-setup
143 1 : .filter(pol => pol.startsWith("FIPS") ? fipsConfigurable : true)
144 1 : .map(policy => ({
145 1 : name: policy,
146 1 : title: displayProfileText(policy),
147 1 : description: cryptopolicies[policy],
148 1 : active: !isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
149 1 : inconsistent: isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
150 1 : recommended: policy === 'DEFAULT',
151 1 : }));
152 :
153 : // Custom profile
154 1 : if (!(currentCryptoPolicy in cryptopolicies)) {
155 1 : policies.push({
156 1 : name: currentCryptoPolicy,
157 1 : title: displayProfileText(currentCryptoPolicy),
158 1 : description: _("Custom cryptographic policy"),
159 1 : active: !isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
160 1 : inconsistent: isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
161 1 : recommended: false,
162 1 : });
163 1 : }
164 :
165 1 : const help = (
166 1 : <Popover
167 1 : id="crypto-policies-help"
168 1 : bodyContent={
169 1 : <div>
170 1 : {_("Cryptographic Policies is a system component that configures the core cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos protocols.")}
171 1 : </div>
172 : }
173 1 : footerContent={
174 1 : <Button component='a'
175 1 : rel="noopener noreferrer" target="_blank"
176 1 : variant='link'
177 1 : isInline
178 1 : icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
179 1 : href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
180 1 : {_("Learn more")}
181 1 : </Button>
182 : }
183 : >
184 1 : <Button icon={<HelpIcon />} variant="plain" aria-label={_("Help")} />
185 1 : </Popover>
186 : );
187 :
188 1 : return (
189 1 : <Modal position="top" variant="medium"
190 1 : className="ct-m-stretch-body"
191 1 : isOpen
192 1 : onClose={Dialogs.close}
193 1 : id="crypto-policy-dialog"
194 : >
195 1 : <ModalHeader title={_("Change cryptographic policy")}
196 1 : help={help}
197 1 : />
198 1 : <ModalBody>
199 1 : {error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
200 1 : {currentCryptoPolicy && <ProfilesMenuDialogBody active_profile={currentCryptoPolicy}
201 1 : change_selected={setSelected}
202 1 : isDisabled={inProgress}
203 1 : profiles={policies} />}
204 1 : </ModalBody>
205 1 : <ModalFooter>
206 1 : {inProgress &&
207 1 : <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
208 1 : {_("Applying new policy... This may take a few minutes.")}
209 1 : </Flex>}
210 1 : <Button id="crypto-policy-save-reboot" variant='primary'
211 0 : onClick={() => setPolicy(selected, setError, setInProgress, fipsConfigurable)}
212 1 : isDisabled={inProgress} isLoading={inProgress}
213 : >
214 1 : {reApply ? _("Reapply and reboot") : _("Apply and reboot")}
215 1 : </Button>
216 1 : <Button variant='link' onClick={Dialogs.close} isDisabled={inProgress}>
217 1 : {_("Cancel")}
218 1 : </Button>
219 1 : </ModalFooter>
220 1 : </Modal>
221 : );
222 1 : };
223 :
224 92 : export const CryptoPolicyStatus = () => {
225 92 : const Dialogs = useDialogs();
226 92 : const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
227 92 : const [fipsEnabled, setFipsEnabled] = useState(null);
228 92 : const [fipsConfigurable, setFipsConfigurable] = useState(null);
229 :
230 92 : useInit(() => {
231 92 : cockpit.file("/etc/crypto-policies/state/current")
232 16 : .watch(content => setCurrentCryptoPolicy(content ? content.trim().split(':', 1)[0] : undefined));
233 86 : getFipsConfigurable().then(v => setFipsConfigurable(v));
234 92 : cockpit.file("/proc/sys/crypto/fips_enabled").read()
235 16 : .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
236 92 : });
237 :
238 86 : if (currentCryptoPolicy === null || fipsConfigurable === null)
239 92 : return null;
240 :
241 16 : if (isInconsistentPolicy(currentCryptoPolicy, fipsEnabled)) {
242 16 : return (
243 16 : <li className="system-health-crypto-policies">
244 16 : <Flex flexWrap={{ default: 'nowrap' }}>
245 16 : <FlexItem><ExclamationTriangleIcon className="crypto-policies-health-card-icon" /></FlexItem>
246 16 : <div>
247 16 : <div id="inconsistent_crypto_policy">
248 16 : {currentCryptoPolicy === "FIPS" ? _("FIPS is not properly enabled") : _("Cryptographic policy is inconsistent")}
249 16 : </div>
250 16 : <Button isInline variant="link" className="pf-v6-u-font-size-sm"
251 0 : onClick={() => Dialogs.show(<CryptoPolicyDialog currentCryptoPolicy={currentCryptoPolicy}
252 0 : fipsEnabled={fipsEnabled}
253 0 : fipsConfigurable={fipsConfigurable}
254 0 : reApply />)}>
255 16 : {_("Review cryptographic policy")}
256 16 : </Button>
257 16 : </div>
258 16 : </Flex>
259 16 : </li>
260 : );
261 16 : }
262 :
263 84 : return null;
264 92 : };
|