Skip to content

Commit 11a44d5

Browse files
authored
Support to config fips state (sonic-net#69)
Support to config fips state, allow to enable or enforce the FIPS. When FIPS is enforced, you can only disable the FIPS by changing to non-enforced state and reboot the DUT. HLD: https://github.com/sonic-net/SONiC/blob/master/doc/fips/SONiC-OpenSSL-FIPS-140-3-deployment.md
1 parent bc08806 commit 11a44d5

9 files changed

Lines changed: 326 additions & 22 deletions

File tree

azure-pipelines.yml

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,30 +75,21 @@ stages:
7575
sudo dpkg -i libnl-nf-3-200_*.deb
7676
sudo dpkg -i libhiredis0.14_*.deb
7777
sudo dpkg -i libyang_1.0.73_*.deb
78-
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
79-
displayName: 'Install Debian dependencies'
80-
81-
- task: DownloadPipelineArtifact@2
82-
inputs:
83-
source: specific
84-
project: build
85-
pipeline: 9
86-
artifact: sonic-swss-common
87-
runVersion: 'latestFromBranch'
88-
runBranch: 'refs/heads/$(BUILD_BRANCH)'
89-
displayName: "Download sonic swss common deb packages"
90-
91-
- script: |
92-
set -xe
9378
sudo dpkg -i libswsscommon_1.0.0_amd64.deb
9479
sudo dpkg -i python3-swsscommon_1.0.0_amd64.deb
95-
workingDirectory: $(Pipeline.Workspace)/
96-
displayName: 'Install swss-common dependencies'
80+
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
81+
displayName: 'Install Debian dependencies'
9782
9883
- script: |
9984
set -xe
85+
sudo pip3 install enum34
10086
sudo pip3 install swsssdk-2.0.1-py3-none-any.whl
10187
sudo pip3 install sonic_py_common-1.0-py3-none-any.whl
88+
sudo pip3 install sonic_yang_mgmt-1.0-py3-none-any.whl
89+
sudo pip3 install sonic_yang_models-1.0-py3-none-any.whl
90+
sudo pip3 install sonic_config_engine-1.0-py3-none-any.whl
91+
sudo pip3 install sonic_platform_common-1.0-py3-none-any.whl
92+
sudo pip3 install sonic_utilities-1.2-py3-none-any.whl
10293
workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bullseye/
10394
displayName: 'Install Python dependencies'
10495

scripts/hostcfgd

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import syslog
1010
import signal
1111
import re
1212
import jinja2
13+
import json
1314
import threading
1415
from shutil import copy2
16+
from datetime import datetime
1517
from sonic_py_common import device_info
1618
from sonic_py_common.general import check_output_pipe
1719
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table, SonicDBConfig
1820
from swsscommon import swsscommon
21+
from sonic_installer import bootloader
1922

2023
# FILE
2124
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
@@ -65,12 +68,18 @@ RADIUS_SERVER_TIMEOUT_DEFAULT = "5"
6568
RADIUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
6669
RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"
6770

71+
# FIPS
72+
FIPS_CONFIG_FILE = '/etc/sonic/fips.json'
73+
OPENSSL_FIPS_CONFIG_FILE = '/etc/fips/fips_enable'
74+
DEFAULT_FIPS_RESTART_SERVICES = ['ssh', 'telemetry.service', 'restapi']
75+
6876
# MISC Constants
6977
CFG_DB = "CONFIG_DB"
7078
STATE_DB = "STATE_DB"
7179
HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd
7280
DEFAULT_SELECT_TIMEOUT = 1000
7381
PORT_INIT_TIMEOUT_SEC = 180
82+
PROC_CMDLINE = '/proc/cmdline'
7483

7584

7685
def safe_eval(val, default_value=False):
@@ -115,6 +124,18 @@ def run_cmd_pipe(cmd0, cmd1, cmd2, log_err=True, raise_exception=False):
115124
if raise_exception:
116125
raise
117126

127+
def run_cmd_output(cmd, log_err=True, raise_exception=False):
128+
output = ''
129+
try:
130+
output = subprocess.check_output(cmd)
131+
except Exception as err:
132+
if log_err:
133+
syslog.syslog(syslog.LOG_ERR, "{} - failed: return code - {}, output:\n{}"
134+
.format(err.cmd, err.returncode, err.output))
135+
if raise_exception:
136+
raise
137+
return output
138+
118139

119140
def is_true(val):
120141
if val == 'True' or val == 'true':
@@ -1766,7 +1787,6 @@ class RSyslogCfg(object):
17661787
self.cache['config'] = rsyslog_config
17671788
self.cache['servers'] = rsyslog_servers
17681789

1769-
17701790
class DnsCfg:
17711791

17721792
def load(self, *args, **kwargs):
@@ -1775,6 +1795,100 @@ class DnsCfg:
17751795
def dns_update(self, *args, **kwargs):
17761796
run_cmd(['systemctl', 'restart', 'resolv-config'], True, False)
17771797

1798+
class FipsCfg(object):
1799+
"""
1800+
FipsCfg Config Daemon
1801+
Handles the changes in FIPS table.
1802+
"""
1803+
1804+
def __init__(self, state_db_conn):
1805+
self.enable = False
1806+
self.enforce = False
1807+
self.restart_services = DEFAULT_FIPS_RESTART_SERVICES
1808+
self.state_db_conn = state_db_conn
1809+
1810+
def read_config(self):
1811+
if os.path.exists(FIPS_CONFIG_FILE):
1812+
with open(FIPS_CONFIG_FILE) as f:
1813+
conf = json.load(f)
1814+
self.restart_services = conf.get(RESTART_SERVICES_KEY, [])
1815+
1816+
with open(PROC_CMDLINE) as f:
1817+
kernel_cmdline = f.read().strip().split(' ')
1818+
self.cur_enforced = 'sonic_fips=1' in kernel_cmdline or 'fips=1' in kernel_cmdline
1819+
1820+
def load(self, data={}):
1821+
common_config = data.get('global', {})
1822+
if not common_config:
1823+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped the FIPS config, the FIPS setting is empty.')
1824+
return
1825+
self.read_config()
1826+
self.enforce = is_true(common_config.get('enforce', 'false'))
1827+
self.enable = self.enforce or is_true(common_config.get('enable', 'false'))
1828+
self.update()
1829+
1830+
def fips_handler(self, data):
1831+
self.load(data)
1832+
1833+
def update(self):
1834+
syslog.syslog(syslog.LOG_DEBUG, f'FipsCfg: update fips option enable: {self.enable}, enforce: {self.enforce}.')
1835+
self.update_enforce_config()
1836+
self.update_noneenforce_config()
1837+
self.state_db_conn.hset('FIPS_STATS|state', 'config_datetime', datetime.utcnow().isoformat())
1838+
syslog.syslog(syslog.LOG_DEBUG, f'FipsCfg: update fips option complete.')
1839+
1840+
def update_noneenforce_config(self):
1841+
cur_fips_enabled = '0'
1842+
if os.path.exists(OPENSSL_FIPS_CONFIG_FILE):
1843+
with open(OPENSSL_FIPS_CONFIG_FILE) as f:
1844+
cur_fips_enabled = f.read().strip()
1845+
1846+
expected_fips_enabled = '0'
1847+
if self.enable:
1848+
expected_fips_enabled = '1'
1849+
1850+
# If the runtime config is not as expected, change the config
1851+
if cur_fips_enabled != expected_fips_enabled:
1852+
os.makedirs(os.path.dirname(OPENSSL_FIPS_CONFIG_FILE), exist_ok=True)
1853+
with open(OPENSSL_FIPS_CONFIG_FILE, 'w') as f:
1854+
f.write(expected_fips_enabled)
1855+
1856+
self.restart()
1857+
1858+
def restart(self):
1859+
if self.cur_enforced:
1860+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to restart services, since FIPS enforced.')
1861+
return
1862+
1863+
modified_time = datetime.utcfromtimestamp(0)
1864+
if os.path.exists(OPENSSL_FIPS_CONFIG_FILE):
1865+
modified_time = datetime.fromtimestamp(os.path.getmtime(OPENSSL_FIPS_CONFIG_FILE))
1866+
timestamp = self.state_db_conn.hget('FIPS_STATS|state', 'config_datetime')
1867+
if timestamp and datetime.fromisoformat(timestamp).replace(tzinfo=None) > modified_time.replace(tzinfo=None):
1868+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to restart services, since the services have alread been restarted.')
1869+
return
1870+
1871+
# Restart the services required and in the running state
1872+
output = run_cmd_output(['sudo', 'systemctl', '-t', 'service', '--state=running', '--no-pager', '-o', 'json'])
1873+
if not output:
1874+
return
1875+
1876+
services = [s['unit'] for s in json.loads(output)]
1877+
for service in self.restart_services:
1878+
if service in services or service + '.service' in services:
1879+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: restart service {service}.')
1880+
run_cmd(['sudo', 'systemctl', 'restart', service])
1881+
1882+
1883+
def update_enforce_config(self):
1884+
loader = bootloader.get_bootloader()
1885+
image = loader.get_next_image()
1886+
next_enforced = loader.get_fips(image)
1887+
if next_enforced == self.enforce:
1888+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to configure the enforce option {self.enforce}, since the config has already been set.')
1889+
return
1890+
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.')
1891+
loader.set_fips(image, self.enforce)
17781892

17791893
class HostConfigDaemon:
17801894
def __init__(self):
@@ -1835,6 +1949,9 @@ class HostConfigDaemon:
18351949
# Initialize DnsCfg
18361950
self.dnscfg = DnsCfg()
18371951

1952+
# Initialize FipsCfg
1953+
self.fipscfg = FipsCfg(self.state_db_conn)
1954+
18381955
def load(self, init_data):
18391956
features = init_data['FEATURE']
18401957
aaa = init_data['AAA']
@@ -1854,6 +1971,7 @@ class HostConfigDaemon:
18541971
syslog_cfg = init_data.get(swsscommon.CFG_SYSLOG_CONFIG_TABLE_NAME, {})
18551972
syslog_srv = init_data.get(swsscommon.CFG_SYSLOG_SERVER_TABLE_NAME, {})
18561973
dns = init_data.get('DNS_NAMESERVER', {})
1974+
fips_cfg = init_data.get('FIPS', {})
18571975

18581976
self.feature_handler.sync_state_field(features)
18591977
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
@@ -1867,6 +1985,7 @@ class HostConfigDaemon:
18671985

18681986
self.rsyslogcfg.load(syslog_cfg, syslog_srv)
18691987
self.dnscfg.load(dns)
1988+
self.fipscfg.load(fips_cfg)
18701989

18711990
# Update AAA with the hostname
18721991
self.aaacfg.hostname_update(self.devmetacfg.hostname)
@@ -1990,6 +2109,11 @@ class HostConfigDaemon:
19902109
syslog.syslog(syslog.LOG_INFO, 'DNS nameserver handler...')
19912110
self.dnscfg.dns_update(key, data)
19922111

2112+
def fips_config_handler(self, key, op, data):
2113+
syslog.syslog(syslog.LOG_INFO, 'FIPS table handler...')
2114+
data = self.config_db.get_table("FIPS")
2115+
self.fipscfg.fips_handler(data)
2116+
19932117
def wait_till_system_init_done(self):
19942118
# No need to print the output in the log file so using the "--quiet"
19952119
# flag
@@ -2046,6 +2170,9 @@ class HostConfigDaemon:
20462170

20472171
self.config_db.subscribe('DNS_NAMESERVER', make_callback(self.dns_nameserver_handler))
20482172

2173+
# Handle FIPS changes
2174+
self.config_db.subscribe('FIPS', make_callback(self.fips_config_handler))
2175+
20492176
syslog.syslog(syslog.LOG_INFO,
20502177
"Waiting for systemctl to finish initialization")
20512178
self.wait_till_system_init_done()
@@ -2055,7 +2182,6 @@ class HostConfigDaemon:
20552182
def start(self):
20562183
self.config_db.listen(init_data_handler=self.load)
20572184

2058-
20592185
def main():
20602186
signal.signal(signal.SIGTERM, signal_handler)
20612187
signal.signal(signal.SIGINT, signal_handler)

scripts/procdockerstatsd

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ from datetime import datetime
1313

1414
from sonic_py_common import daemon_base
1515
from swsscommon import swsscommon
16-
from sonic_py_common.general import getstatusoutput_noshell_pipe
16+
from sonic_py_common.general import getstatusoutput_noshell_pipe, getstatusoutput_noshell
1717

1818
VERSION = '1.0'
1919

@@ -168,6 +168,22 @@ class ProcDockerStats(daemon_base.DaemonBase):
168168
cmd = row.get('CMD')
169169
self.update_state_db(value, 'CMD', cmd)
170170

171+
def update_fipsstats_command(self):
172+
fips_db_key = 'FIPS_STATS|state'
173+
174+
# Check if FIPS enforced in the current kernel cmdline
175+
with open('/proc/cmdline') as f:
176+
kernel_cmdline = f.read().strip().split(' ')
177+
enforced = 'sonic_fips=1' in kernel_cmdline or 'fips=1' in kernel_cmdline
178+
179+
# Check if FIPS runtime status
180+
exitcode, _ = getstatusoutput_noshell_pipe(['sudo', 'openssl', 'engine', '-vv'], ['grep', '-i', 'symcryp'])
181+
enabled = not any(exitcode)
182+
183+
self.update_state_db(fips_db_key, 'timestamp', datetime.utcnow().isoformat())
184+
self.update_state_db(fips_db_key, 'enforced', str(enforced))
185+
self.update_state_db(fips_db_key, 'enabled', str(enabled))
186+
171187
def update_state_db(self, key1, key2, value2):
172188
self.state_db.set('STATE_DB', key1, key2, value2)
173189

@@ -186,6 +202,8 @@ class ProcDockerStats(daemon_base.DaemonBase):
186202
self.update_state_db('DOCKER_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))
187203
self.update_processstats_command()
188204
self.update_state_db('PROCESS_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))
205+
self.update_fipsstats_command()
206+
self.update_state_db('FIPS_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))
189207

190208
# Data need to be updated every 2 mins. hence adding delay of 120 seconds
191209
time.sleep(120)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from packaging import version
66

77
# sonic_dependencies, version requirement only supports '>='
8-
sonic_dependencies = ['sonic-py-common']
8+
sonic_dependencies = ['sonic-py-common', 'sonic-utilities']
99
for package in sonic_dependencies:
1010
try:
1111
package_dist = pkg_resources.get_distribution(package.split(">=")[0])

tests/common/mock_bootloader.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class MockBootloader(object):
2+
3+
def __init__(self, enforce=False):
4+
self.enforce = enforce
5+
6+
def get_next_image(self):
7+
return ""
8+
9+
def set_fips(self, image, enable):
10+
self.enforce = enable
11+
12+
def get_fips(self, image):
13+
return self.enforce

tests/common/mock_configdb.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,16 @@ def pop(self):
139139

140140
class MockDBConnector():
141141
def __init__(self, db, val, tcpFlag=False, name=None):
142-
pass
142+
self.data = {}
143+
144+
def hget(self, key, field):
145+
if key not in self.data:
146+
return None
147+
if field not in self.data[key]:
148+
return None
149+
return self.data[key][field]
150+
151+
def hset(self, key, field, value):
152+
if key not in self.data:
153+
self.data[key] = {}
154+
self.data[key][field] = value

0 commit comments

Comments
 (0)