Skip to content

Commit

Permalink
Support to config fips state (#69)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
xumia authored Jul 21, 2023
1 parent bc08806 commit 11a44d5
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 22 deletions.
25 changes: 8 additions & 17 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,30 +75,21 @@ stages:
sudo dpkg -i libnl-nf-3-200_*.deb
sudo dpkg -i libhiredis0.14_*.deb
sudo dpkg -i libyang_1.0.73_*.deb
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
displayName: 'Install Debian dependencies'
- task: DownloadPipelineArtifact@2
inputs:
source: specific
project: build
pipeline: 9
artifact: sonic-swss-common
runVersion: 'latestFromBranch'
runBranch: 'refs/heads/$(BUILD_BRANCH)'
displayName: "Download sonic swss common deb packages"

- script: |
set -xe
sudo dpkg -i libswsscommon_1.0.0_amd64.deb
sudo dpkg -i python3-swsscommon_1.0.0_amd64.deb
workingDirectory: $(Pipeline.Workspace)/
displayName: 'Install swss-common dependencies'
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
displayName: 'Install Debian dependencies'
- script: |
set -xe
sudo pip3 install enum34
sudo pip3 install swsssdk-2.0.1-py3-none-any.whl
sudo pip3 install sonic_py_common-1.0-py3-none-any.whl
sudo pip3 install sonic_yang_mgmt-1.0-py3-none-any.whl
sudo pip3 install sonic_yang_models-1.0-py3-none-any.whl
sudo pip3 install sonic_config_engine-1.0-py3-none-any.whl
sudo pip3 install sonic_platform_common-1.0-py3-none-any.whl
sudo pip3 install sonic_utilities-1.2-py3-none-any.whl
workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bullseye/
displayName: 'Install Python dependencies'
Expand Down
130 changes: 128 additions & 2 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import syslog
import signal
import re
import jinja2
import json
import threading
from shutil import copy2
from datetime import datetime
from sonic_py_common import device_info
from sonic_py_common.general import check_output_pipe
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table, SonicDBConfig
from swsscommon import swsscommon
from sonic_installer import bootloader

# FILE
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
Expand Down Expand Up @@ -65,12 +68,18 @@ RADIUS_SERVER_TIMEOUT_DEFAULT = "5"
RADIUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"

# FIPS
FIPS_CONFIG_FILE = '/etc/sonic/fips.json'
OPENSSL_FIPS_CONFIG_FILE = '/etc/fips/fips_enable'
DEFAULT_FIPS_RESTART_SERVICES = ['ssh', 'telemetry.service', 'restapi']

# MISC Constants
CFG_DB = "CONFIG_DB"
STATE_DB = "STATE_DB"
HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd
DEFAULT_SELECT_TIMEOUT = 1000
PORT_INIT_TIMEOUT_SEC = 180
PROC_CMDLINE = '/proc/cmdline'


def safe_eval(val, default_value=False):
Expand Down Expand Up @@ -115,6 +124,18 @@ def run_cmd_pipe(cmd0, cmd1, cmd2, log_err=True, raise_exception=False):
if raise_exception:
raise

def run_cmd_output(cmd, log_err=True, raise_exception=False):
output = ''
try:
output = subprocess.check_output(cmd)
except Exception as err:
if log_err:
syslog.syslog(syslog.LOG_ERR, "{} - failed: return code - {}, output:\n{}"
.format(err.cmd, err.returncode, err.output))
if raise_exception:
raise
return output


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


class DnsCfg:

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

class FipsCfg(object):
"""
FipsCfg Config Daemon
Handles the changes in FIPS table.
"""

def __init__(self, state_db_conn):
self.enable = False
self.enforce = False
self.restart_services = DEFAULT_FIPS_RESTART_SERVICES
self.state_db_conn = state_db_conn

def read_config(self):
if os.path.exists(FIPS_CONFIG_FILE):
with open(FIPS_CONFIG_FILE) as f:
conf = json.load(f)
self.restart_services = conf.get(RESTART_SERVICES_KEY, [])

with open(PROC_CMDLINE) as f:
kernel_cmdline = f.read().strip().split(' ')
self.cur_enforced = 'sonic_fips=1' in kernel_cmdline or 'fips=1' in kernel_cmdline

def load(self, data={}):
common_config = data.get('global', {})
if not common_config:
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped the FIPS config, the FIPS setting is empty.')
return
self.read_config()
self.enforce = is_true(common_config.get('enforce', 'false'))
self.enable = self.enforce or is_true(common_config.get('enable', 'false'))
self.update()

def fips_handler(self, data):
self.load(data)

def update(self):
syslog.syslog(syslog.LOG_DEBUG, f'FipsCfg: update fips option enable: {self.enable}, enforce: {self.enforce}.')
self.update_enforce_config()
self.update_noneenforce_config()
self.state_db_conn.hset('FIPS_STATS|state', 'config_datetime', datetime.utcnow().isoformat())
syslog.syslog(syslog.LOG_DEBUG, f'FipsCfg: update fips option complete.')

def update_noneenforce_config(self):
cur_fips_enabled = '0'
if os.path.exists(OPENSSL_FIPS_CONFIG_FILE):
with open(OPENSSL_FIPS_CONFIG_FILE) as f:
cur_fips_enabled = f.read().strip()

expected_fips_enabled = '0'
if self.enable:
expected_fips_enabled = '1'

# If the runtime config is not as expected, change the config
if cur_fips_enabled != expected_fips_enabled:
os.makedirs(os.path.dirname(OPENSSL_FIPS_CONFIG_FILE), exist_ok=True)
with open(OPENSSL_FIPS_CONFIG_FILE, 'w') as f:
f.write(expected_fips_enabled)

self.restart()

def restart(self):
if self.cur_enforced:
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to restart services, since FIPS enforced.')
return

modified_time = datetime.utcfromtimestamp(0)
if os.path.exists(OPENSSL_FIPS_CONFIG_FILE):
modified_time = datetime.fromtimestamp(os.path.getmtime(OPENSSL_FIPS_CONFIG_FILE))
timestamp = self.state_db_conn.hget('FIPS_STATS|state', 'config_datetime')
if timestamp and datetime.fromisoformat(timestamp).replace(tzinfo=None) > modified_time.replace(tzinfo=None):
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to restart services, since the services have alread been restarted.')
return

# Restart the services required and in the running state
output = run_cmd_output(['sudo', 'systemctl', '-t', 'service', '--state=running', '--no-pager', '-o', 'json'])
if not output:
return

services = [s['unit'] for s in json.loads(output)]
for service in self.restart_services:
if service in services or service + '.service' in services:
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: restart service {service}.')
run_cmd(['sudo', 'systemctl', 'restart', service])


def update_enforce_config(self):
loader = bootloader.get_bootloader()
image = loader.get_next_image()
next_enforced = loader.get_fips(image)
if next_enforced == self.enforce:
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: skipped to configure the enforce option {self.enforce}, since the config has already been set.')
return
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.')
loader.set_fips(image, self.enforce)

class HostConfigDaemon:
def __init__(self):
Expand Down Expand Up @@ -1835,6 +1949,9 @@ class HostConfigDaemon:
# Initialize DnsCfg
self.dnscfg = DnsCfg()

# Initialize FipsCfg
self.fipscfg = FipsCfg(self.state_db_conn)

def load(self, init_data):
features = init_data['FEATURE']
aaa = init_data['AAA']
Expand All @@ -1854,6 +1971,7 @@ class HostConfigDaemon:
syslog_cfg = init_data.get(swsscommon.CFG_SYSLOG_CONFIG_TABLE_NAME, {})
syslog_srv = init_data.get(swsscommon.CFG_SYSLOG_SERVER_TABLE_NAME, {})
dns = init_data.get('DNS_NAMESERVER', {})
fips_cfg = init_data.get('FIPS', {})

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

self.rsyslogcfg.load(syslog_cfg, syslog_srv)
self.dnscfg.load(dns)
self.fipscfg.load(fips_cfg)

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

def fips_config_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'FIPS table handler...')
data = self.config_db.get_table("FIPS")
self.fipscfg.fips_handler(data)

def wait_till_system_init_done(self):
# No need to print the output in the log file so using the "--quiet"
# flag
Expand Down Expand Up @@ -2046,6 +2170,9 @@ class HostConfigDaemon:

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

# Handle FIPS changes
self.config_db.subscribe('FIPS', make_callback(self.fips_config_handler))

syslog.syslog(syslog.LOG_INFO,
"Waiting for systemctl to finish initialization")
self.wait_till_system_init_done()
Expand All @@ -2055,7 +2182,6 @@ class HostConfigDaemon:
def start(self):
self.config_db.listen(init_data_handler=self.load)


def main():
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
Expand Down
20 changes: 19 additions & 1 deletion scripts/procdockerstatsd
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ from datetime import datetime

from sonic_py_common import daemon_base
from swsscommon import swsscommon
from sonic_py_common.general import getstatusoutput_noshell_pipe
from sonic_py_common.general import getstatusoutput_noshell_pipe, getstatusoutput_noshell

VERSION = '1.0'

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

def update_fipsstats_command(self):
fips_db_key = 'FIPS_STATS|state'

# Check if FIPS enforced in the current kernel cmdline
with open('/proc/cmdline') as f:
kernel_cmdline = f.read().strip().split(' ')
enforced = 'sonic_fips=1' in kernel_cmdline or 'fips=1' in kernel_cmdline

# Check if FIPS runtime status
exitcode, _ = getstatusoutput_noshell_pipe(['sudo', 'openssl', 'engine', '-vv'], ['grep', '-i', 'symcryp'])
enabled = not any(exitcode)

self.update_state_db(fips_db_key, 'timestamp', datetime.utcnow().isoformat())
self.update_state_db(fips_db_key, 'enforced', str(enforced))
self.update_state_db(fips_db_key, 'enabled', str(enabled))

def update_state_db(self, key1, key2, value2):
self.state_db.set('STATE_DB', key1, key2, value2)

Expand All @@ -186,6 +202,8 @@ class ProcDockerStats(daemon_base.DaemonBase):
self.update_state_db('DOCKER_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))
self.update_processstats_command()
self.update_state_db('PROCESS_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))
self.update_fipsstats_command()
self.update_state_db('FIPS_STATS|LastUpdateTime', 'lastupdate', str(datetimeobj))

# Data need to be updated every 2 mins. hence adding delay of 120 seconds
time.sleep(120)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from packaging import version

# sonic_dependencies, version requirement only supports '>='
sonic_dependencies = ['sonic-py-common']
sonic_dependencies = ['sonic-py-common', 'sonic-utilities']
for package in sonic_dependencies:
try:
package_dist = pkg_resources.get_distribution(package.split(">=")[0])
Expand Down
13 changes: 13 additions & 0 deletions tests/common/mock_bootloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class MockBootloader(object):

def __init__(self, enforce=False):
self.enforce = enforce

def get_next_image(self):
return ""

def set_fips(self, image, enable):
self.enforce = enable

def get_fips(self, image):
return self.enforce
14 changes: 13 additions & 1 deletion tests/common/mock_configdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,16 @@ def pop(self):

class MockDBConnector():
def __init__(self, db, val, tcpFlag=False, name=None):
pass
self.data = {}

def hget(self, key, field):
if key not in self.data:
return None
if field not in self.data[key]:
return None
return self.data[key][field]

def hset(self, key, field, value):
if key not in self.data:
self.data[key] = {}
self.data[key][field] = value
Loading

0 comments on commit 11a44d5

Please sign in to comment.