Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support to config fips state (#69) #77

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 129 additions & 1 deletion scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import syslog
import signal
import re
import jinja2
import json
import threading
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 @@ -57,12 +60,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 @@ -107,6 +116,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 @@ -1660,6 +1681,101 @@ class RSyslogCfg(object):
self.cache['config'] = rsyslog_config
self.cache['servers'] = rsyslog_servers

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):
self.state_db_conn = DBConnector(STATE_DB, 0)
Expand Down Expand Up @@ -1713,6 +1829,9 @@ class HostConfigDaemon:
# Initialize RSyslogCfg
self.rsyslogcfg = RSyslogCfg()

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

def load(self, init_data):
features = init_data['FEATURE']
aaa = init_data['AAA']
Expand All @@ -1730,7 +1849,7 @@ class HostConfigDaemon:
mgmt_vrf = init_data.get(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, {})
syslog_cfg = init_data.get(swsscommon.CFG_SYSLOG_CONFIG_TABLE_NAME, {})
syslog_srv = init_data.get(swsscommon.CFG_SYSLOG_SERVER_TABLE_NAME, {})

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 @@ -1741,6 +1860,7 @@ class HostConfigDaemon:
self.devmetacfg.load(dev_meta)
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)
self.rsyslogcfg.load(syslog_cfg, syslog_srv)
self.fipscfg.load(fips_cfg)

# Update AAA with the hostname
self.aaacfg.hostname_update(self.devmetacfg.hostname)
Expand Down Expand Up @@ -1856,6 +1976,11 @@ class HostConfigDaemon:
syslog.syslog(syslog.LOG_INFO, 'SYSLOG_CONFIG table handler...')
self.rsyslog_handler()

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 @@ -1909,6 +2034,9 @@ class HostConfigDaemon:
self.config_db.subscribe(swsscommon.CFG_SYSLOG_SERVER_TABLE_NAME,
make_callback(self.rsyslog_server_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 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
Loading