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

Container net host remove #15176

Closed

Conversation

ycoheNvidia
Copy link
Contributor

@ycoheNvidia ycoheNvidia commented May 21, 2023

submodule change to support snmp container hardening: sonic-net/sonic-snmpagent#281

Why I did it

Net host removal for snmp and database as part of container hardening

How I did it

How to verify it

Modified snmp related templates to support net host removal:

  • SNMP Docker listening-address:port forwarding
  • snmpd.conf listening address to include all addresses - since docker port forwarding is the actual routing table - and blocks all other addresses.
  • $NET modified to bridge for snmp and database container
  • snmp docker stop to rm container - since port forwarding can only happen on newly created image

Description for the changelog

snmp and database host net removal

A picture of a cute animal (not mandatory but encouraged)

   .
  ":"
___:____     |"\/"|

,' `. \ /
| O ___/ |
^^^^^^^^^^^^~

@@ -32,6 +32,8 @@ RUN apt-get clean -y && \
s/^# unixsocket/unixsocket/; \
s/redis-server.sock/redis.sock/g; \
s/^client-output-buffer-limit pubsub [0-9]+mb [0-9]+mb [0-9]+/client-output-buffer-limit pubsub 0 0 0/; \
s/^bind 127.0.0.1 ::1$/# bind 127.0.0.1 ::1/; \
s/^protected-mode yes/protected-mode no/; \
Copy link
Collaborator

@qiluo-msft qiluo-msft Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no

This is not safe, right? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis protected mode only prevents the Redis server from responding to queries outside the loopback interfaces.
Docker network mode was changed from host to bridged. This requires to be able to connect from host IP to Redis docker IP.

#
{% if SNMP_AGENT_ADDRESS_CONFIG %}
{% for (agentip, port, vrf) in SNMP_AGENT_ADDRESS_CONFIG %}
Copy link
Collaborator

@qiluo-msft qiluo-msft Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SNMP_AGENT_ADDRESS_CONFIG

This feature is still useful, and there are some other PRs trying to extend it. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, I wasn't aware of the other PR's for this feature.
The logic of address/port/vrf was moved to the docker create port and address forwarding. Since only user defined ports and addresses will be forwarded, this logic will no longer be relevant here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the code to include the latest merged PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain "The logic of address/port/vrf was moved to the docker create port and address forwarding"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current implementation, snmpd has its configuration in snmpd.conf which defines which addresses and ports can be used for query, how and if vrf is being used, etc.
Removing snmp container from host network means that it is not exposed to any of these addresses by default,
What we did here was to use docker address:port forwarding as the method to implement the logic for it. When a user configures address and port to be used for snmp queries - this address:port tuple will be used when snmp docker is created, similar to a firewall. In what we offer here, the snmp demon inside the container will listen to all packets, and the docker networking logic (address:port forwarding) will forward only the relevant packets, as configured by user

NET="host"
{%- if docker_container_name == "database" %}
NET="bridge"
PORT_MAP="-p 127.0.0.1:6379:6379"
Copy link
Collaborator

@qiluo-msft qiluo-msft Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6379

multi-DB usage may require more ports. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean multi-asic case. Its working. Each asic db docker has its own IP with mapped 6379 listening port and shared redis.sock socket file.
If there are more use cases, that may require different ports please elaborate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to fetch the ports for all redis instances from conf files: "/var/run/redis/sonic-db/database_config.json".

With multi-DB feature, there will be multiple-namespace, and multiple instance per namespace. ref: https://github.com/sonic-net/SONiC/blob/master/doc/database/multi_namespace_db_instances.md

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still applicable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, checking

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still applicable

@@ -32,6 +32,8 @@ RUN apt-get clean -y && \
s/^# unixsocket/unixsocket/; \
s/redis-server.sock/redis.sock/g; \
s/^client-output-buffer-limit pubsub [0-9]+mb [0-9]+mb [0-9]+/client-output-buffer-limit pubsub 0 0 0/; \
s/^bind 127.0.0.1 ::1$/# bind 127.0.0.1 ::1/; \
Copy link
Collaborator

@qiluo-msft qiluo-msft Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bind 127.0.0.1 ::1

It will bind to all the interfaces visibile to database container. To be super cautious, could you add a sonic-mgmt testcase to ensure redis is not expose external port on sonic? #Closed

fi
{%- if docker_container_name == "snmp" %}
# get snmp listening address and port list from redis
addr_port_values=$(python3 -c 'from swsscommon.swsscommon import ConfigDBConnector; cfg_db = ConfigDBConnector(); cfg_db.connect(wait_for_init=True, retry_on=True); [print(k[0] + "|" + k[1]) for k in cfg_db.get_keys("SNMP_AGENT_ADDRESS_CONFIG|*")]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this logic moved here instead of doing in snmpd.conf.j2 file?
What is the value add we get?
This is done during docker create. If we are just restarting snmp server and there is a change in the SNMP_AGENT_ADDRESS_CONFIG in config_db, will the new change get picked up if we just restart running docker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved here because port and address forwarding can only be done during docker create. What it means that in order to harden this container's network - each address/port change for this service will require docker removal and creation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR will kill the feature of SNMP_AGENT_ADDRESS_CONFIG/port. Do you want to implement the same feature here or in another PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand your question. This PR and sonic-net/sonic-snmpagent#281 moves the logic of snmp networking management from the container to the host. The host will forward the relevant packets only (SNMP_AGENT_ADDRESS_CONFIG for example) and the container will take all packets sent to port 161

@@ -523,6 +531,8 @@ start() {
{%- if docker_container_name == "database" %}
NET="bridge"
DB_OPT=$DB_OPT" -v /var/run/redis$DEV:/var/run/redis:rw "
{%- elif docker_container_name == "snmp" %}
NET="bridge"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NET="bridge"

Checking best practice (https://docs.docker.com/network/network-tutorial-standalone/), default bridge network is not best choice for production. User-defined bridge networks is best choice for production. Can we use user-defined bridge network here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, defining a user-defined bridge is the best case solution for this item.
Problem is - this FR wasn't scoped to define this user-bridge and therefor remained in the general bridge domain.
If such a bridge will be defined - then we can add it to the SNMP container and redis container (and any future container) as well.

@ycoheNvidia
Copy link
Contributor Author

@qiluo-msft we addressed the comments and I adjusted the code to the recent merged snmp code. Can we proceed please?

@qiluo-msft
Copy link
Collaborator

qiluo-msft commented Aug 11, 2023

Please resolove conflict #Closed

NAMESPACE_COUNT=$NUM_ASIC
if [ -z $addr_port_values ]; then
if [ -z $NAMESPACE_COUNT ] || [ $NAMESPACE_COUNT -lt 2 ]; then
addr_port_values=$(python3 -c 'from swsscommon.swsscommon import ConfigDBConnector; cfg_db = ConfigDBConnector(); cfg_db.connect(wait_for_init=True, retry_on=True); [print(k[1].split("/")[0].lower() + "%" + k[0]) if len(k) == 2 and k[1].split('/')[0].lower().startswith("fe80") else print(k[1].split("/")[0].lower()) for k in cfg_db.get_keys("LOOPBACK_INTERFACE|*")+cfg_db.get_keys("MGMT_INTERFACE|*") if len(k) == 2]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here currently only Loopback and Mgmt IP addresses are used.
There is SNMP_AGENT_ADDRESS_CONFIG table which can be configured with the IPs for snmpd to listen on.
This change does not include using that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SuvarnaMeenakshi you either missed line 556 which addresses the specific SNMP_AGENT_ADDRESS_CONFIG table, or I miss something. I followed the logic that was found in snmpd.conf.j2 and translated it here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is throwing a syntax error.
python3 -c 'from swsscommon.swsscommon import ConfigDBConnector; cfg_db = ConfigDBConnector(); cfg_db.connect(wait_for_init=True, retry_on=True); [print(k[1].split("/")[0].lower() + "%" + k[0]) if len(k) == 2 and k[1].split('/')[0].lower().startswith("fe80") else print(k[1].split("/")[0].lower()) for k in cfg_db.get_keys("LOOPBACK_INTERFACE|")+cfg_db.get_keys("MGMT_INTERFACE|") if len(k) == 2]'

Why do we need the "fe80" check?
We can keep the fe80 condition in the snmpd.conf.j2 file?

@yaqiangz
Copy link
Contributor

yaqiangz commented Sep 14, 2023

Hi @ycoheNvidia, I wonder whether syslog generated inside bridge mode container can be written to host machine and update to remote syslog server?

@@ -36,7 +36,7 @@ dependent_startup=true
{%- else -%}
{%- set LOOPBACK_IP = '' -%}
{%- endif -%}
command=/bin/bash -c "{ [[ -s /var/lib/{{ redis_inst }}/dump.rdb ]] || rm -f /var/lib/{{ redis_inst }}/dump.rdb; } && mkdir -p /var/lib/{{ redis_inst }} && exec /usr/bin/redis-server /etc/redis/redis.conf --bind {{ LOOPBACK_IP }} {{ redis_items['hostname'] }} --port {{ redis_items['port'] }} --unixsocket {{ redis_items['unix_socket_path'] }} --pidfile /var/run/redis/{{ redis_inst }}.pid --dir /var/lib/{{ redis_inst }}"
command=/bin/bash -c "{ [[ -s /var/lib/{{ redis_inst }}/dump.rdb ]] || rm -f /var/lib/{{ redis_inst }}/dump.rdb; } && mkdir -p /var/lib/{{ redis_inst }} && exec /usr/bin/redis-server /etc/redis/redis.conf --port {{ redis_items['port'] }} --unixsocket {{ redis_items['unix_socket_path'] }} --pidfile /var/run/redis/{{ redis_inst }}.pid --dir /var/lib/{{ redis_inst }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SONiC Chassis, the SWSS container on the linecard connect to local database as well as the database_chassis instance on supervisor. This info is got from database_global.json. Will this be possible after this change

snippet from database_global.json on linecard

INSTANCES": {
        "redis": {
            "hostname": "127.0.0.1",
            "port": 6379,
            "unix_socket_path": "/var/run/redis/redis.sock",
            "persistence_for_warm_boot": "yes"
        },
        "redis_chassis": {
            "hostname": "redis_chassis.server",
            "port": 6380,
            "unix_socket_path": "/var/run/redis-chassis/redis_chassis.sock",
            "persistence_for_warm_boot": "yes"
        }
    },

NET="host"
{%- if docker_container_name == "database" %}
NET="bridge"
PORT_MAP="-p 127.0.0.1:6379:6379"
Copy link
Collaborator

@qiluo-msft qiluo-msft Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mix tabs and spaces. #Closed

@@ -646,6 +705,9 @@ stop() {
/usr/local/bin/container stop -t 60 $DOCKERNAME
{%- else %}
/usr/local/bin/container stop $DOCKERNAME
{%- if docker_container_name == "snmp" %}
docker rm $DOCKERNAME
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker rm

Why remove docker container?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker container behavior requires that port forwarding is done during "docker run", and cannot be changed afterwards. Using port forwarding as a method of support net host remove will require removing docker stop, as it is called as part of service restart (docker stop + docker run).
An example for flow:

  1. snmp being configured -> snmp service established -> snmp docker created with "docker run" with relevant port forwarding arguments
  2. snmp configuration changes -> snmp service restart -> snmp old docker deleted + calling "docker run" with new port forwarding values

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is hot patches inside docker container, original behavior is that the patches will survive config reload or reboot. But this PR lose the capabilities.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there real use-cases where a docker is being loaded with patches inside it?
There might also be some local files (such as temp config and such in every container), is this a real use-case?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is possible in production environment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still applicable.

NET="bridge"
PORT_MAP="-p 127.0.0.1:6379:6379"
{%- elif docker_container_name == "snmp" %}
NET="bridge"
Copy link
Contributor

@yaqiangz yaqiangz Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried image built by this PR yesterday, version: SONiC.master-15176.360116-6bb24c135. Looks like rsyslog inside snmp container cannot work well. I cannot find any syslog generated by snmp container (contains 'snmp#') in local host syslog file or our syslog server, and process containercfgd which is to dynamically generate rsyslog configuration file inside container doesn't startup. Not sure whether you have verified this, could you confirm that?
@Yarden-Z FYI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ycoheNvidia could check if change like this is needed. #4738

@SuvarnaMeenakshi
Copy link
Contributor

SuvarnaMeenakshi commented Oct 10, 2023

@ycoheNvidia 2 issues that I observed when using this diff with latest master branch image:

  1. redis-connection is not working from snmp_subagent even with the PR change in snmp submodule, I added a comment on that PR. SNMP hardening - redis socket connect sonic-snmpagent#281
  2. SNMP walk is failing for single-asic and multi-asic device, I tried adding iptable NAT rule to modify destination address as eth0 IP address inside snmp docker and modify the SRC while sending out response to docker0 IP, with this I see that snmp query works fine on both single and multi-asic Wondering if you verified the snmp query with these changes?
    sudo ip6tables -t nat -A PREROUTING -p udp -m udp --dport 161 -j DNAT --to-destination fd00::242:f07f:102 (eth0 IP of snmp docker )
    sudo ip6tables -t nat -A POSTROUTING -p tcp -m tcp --dport 161 -j SNAT --to-source fd00::1 (docker0 Ipv6)
    sudo iptables -t nat -A PREROUTING -p udp -m udp --dport 161 -j DNAT --to-destination 240.127.1.2 (eth0 IP of snmp docker)
    sudo iptables -t nat -A POSTROUTING -p udp -m udp --dport 161 -j SNAT --to-source 240.127.1.1 (docker0 Ipv4)
    We need to check if these rules have to added as postStartAction in /usr/bin/snmp.sh after starting docker or if they should be added in caclmgrd. Something similar is done for multi-asic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants