summaryrefslogtreecommitdiff
path: root/python/vyos/utils/strip_config.py
blob: 32f0765645f37c7679abc835b6833b2b96cbbb77 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/python3
#
# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see <http://www.gnu.org/licenses/>.

# XXX: these functions assume that the config is at the top level,
# and aren't capable of anonymizing config subtress.
# They shouldn't be used as a basis for a strip-private filter
# until we figure out if we can pass the config path information to the filter.

import sys
import copy

import vyos.configtree


def __anonymize_password(v):
    return "<PASSWORD REDACTED>"

def __anonymize_key(v):
    return "<KEY DATA REDACTED>"

def __anonymize_data(v):
    return "<DATA REDACTED>"

__secret_paths = [
  # System user password hashes
  {"base_path": ['system', 'login', 'user'], "secret_path": ["authentication", "encrypted-password"], "func": __anonymize_password},

  # PKI data
  {"base_path": ["pki", "ca"], "secret_path": ["private", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "ca"], "secret_path": ["certificate"], "func": __anonymize_key},
  {"base_path": ["pki", "ca"], "secret_path": ["crl"], "func": __anonymize_key},
  {"base_path": ["pki", "certificate"], "secret_path": ["private", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "certificate"], "secret_path": ["certificate"], "func": __anonymize_key},
  {"base_path": ["pki", "certificate"], "secret_path": ["acme", "email"], "func": __anonymize_data},
  {"base_path": ["pki", "key-pair"], "secret_path": ["private", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "key-pair"], "secret_path": ["public", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "openssh"], "secret_path": ["private", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "openssh"], "secret_path": ["public", "key"], "func": __anonymize_key},
  {"base_path": ["pki", "openvpn", "shared-secret"], "secret_path": ["key"], "func": __anonymize_key},
  {"base_path": ["pki", "dh"], "secret_path": ["parameters"], "func": __anonymize_key},

  # IPsec pre-shared secrets
  {"base_path": ['vpn', 'ipsec', 'authentication', 'psk'], "secret_path": ["secret"], "func": __anonymize_password},

  # IPsec x509 passphrases
  {"base_path": ['vpn', 'ipsec', 'site-to-site', 'peer'], "secret_path": ['authentication', 'x509'], "func": __anonymize_password},

  # IPsec remote-access secrets and passwords
  {"base_path": ["vpn", "ipsec", "remote-access", "connection"], "secret_path": ["authentication", "pre-shared-secret"], "func": __anonymize_password},
  # Passwords in remote-access IPsec local users have their own fixup
  # due to deeper nesting.

  # PPTP passwords
  {"base_path": ['vpn', 'pptp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},

  # L2TP passwords
  {"base_path": ['vpn', 'l2tp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
  {"path": ['vpn', 'l2tp', 'remote-access', 'ipsec-settings', 'authentication', 'pre-shared-secret'], "func": __anonymize_password},

  # SSTP passwords
  {"base_path": ['vpn', 'sstp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},

  # OpenConnect passwords
  {"base_path": ['vpn', 'openconnect', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},

  # PPPoE server passwords
  {"base_path": ['service', 'pppoe-server', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},

  # RADIUS PSKs for VPN services
  {"base_path": ["vpn", "sstp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
  {"base_path": ["vpn", "l2tp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
  {"base_path": ["vpn", "pptp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
  {"base_path": ["vpn", "openconnect", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
  {"base_path": ["service", "ipoe-server", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
  {"base_path": ["service", "pppoe-server", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},

  # VRRP passwords
  {"base_path": ['high-availability', 'vrrp', 'group'], "secret_path": ['authentication', 'password'], "func": __anonymize_password},

  # BGP neighbor and peer group passwords
  {"base_path": ['protocols', 'bgp', 'neighbor'], "secret_path": ["password"], "func": __anonymize_password},
  {"base_path": ['protocols', 'bgp', 'peer-group'], "secret_path": ["password"], "func": __anonymize_password},

  # WireGuard private keys
  {"base_path": ["interfaces", "wireguard"], "secret_path": ["private-key"], "func": __anonymize_password},

  # NHRP passwords
  {"base_path": ["protocols", "nhrp", "tunnel"], "secret_path": ["cisco-authentication"], "func": __anonymize_password},

  # RIP passwords
  {"base_path": ["protocols", "rip", "interface"], "secret_path": ["authentication", "plaintext-password"], "func": __anonymize_password},

  # IS-IS passwords
  {"path": ["protocols", "isis", "area-password", "plaintext-password"], "func": __anonymize_password},
  {"base_path": ["protocols", "isis", "interface"], "secret_path": ["password", "plaintext-password"], "func": __anonymize_password},

  # HTTP API servers
  {"base_path": ["service", "https", "api", "keys", "id"], "secret_path": ["key"], "func": __anonymize_password},

  # Telegraf
  {"path": ["service", "monitoring", "telegraf", "prometheus-client", "authentication", "password"], "func": __anonymize_password},
  {"path": ["service", "monitoring", "telegraf", "influxdb", "authentication", "token"], "func": __anonymize_password},
  {"path": ["service", "monitoring", "telegraf", "azure-data-explorer", "authentication", "client-secret"], "func": __anonymize_password},
  {"path": ["service", "monitoring", "telegraf", "splunk", "authentication", "token"], "func": __anonymize_password},

  # SNMPv3 passwords
  {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["privacy", "encrypted-password"], "func": __anonymize_password},
  {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["privacy", "plaintext-password"], "func": __anonymize_password},
  {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["auth", "encrypted-password"], "func": __anonymize_password},
  {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["auth", "encrypted-password"], "func": __anonymize_password},
]

def __prepare_secret_paths(config_tree, secret_paths):
    """ Generate a list of secret paths for the current system,
        adjusted for variable parts such as VRFs and remote access IPsec instances
    """

    # Fixup for remote-access IPsec local users that are nested under two tag nodes
    # We generate the list of their paths dynamically
    ipsec_ra_base = {"base_path": ["vpn", "ipsec", "remote-access", "connection"], "func": __anonymize_password}
    if config_tree.exists(ipsec_ra_base["base_path"]):
        for conn in config_tree.list_nodes(ipsec_ra_base["base_path"]):
            if config_tree.exists(ipsec_ra_base["base_path"] + [conn] + ["authentication", "local-users", "username"]):
                for u in config_tree.list_nodes(ipsec_ra_base["base_path"] + [conn] + ["authentication", "local-users", "username"]):
                    p = copy.copy(ipsec_ra_base)
                    p["base_path"] = p["base_path"] + [conn] + ["authentication", "local-users", "username"]
                    p["secret_path"] = ["password"]
                    secret_paths.append(p)

    # Fixup for VRFs that may contain routing protocols and other nodes nested under them
    vrf_paths = []
    vrf_base_path = ["vrf", "name"]
    if config_tree.exists(vrf_base_path):
        for v in config_tree.list_nodes(vrf_base_path):
            vrf_secret_paths = copy.deepcopy(secret_paths)
            for sp in vrf_secret_paths:
                if "base_path" in sp:
                    sp["base_path"] = vrf_base_path + [v] + sp["base_path"]
                elif "path" in sp:
                    sp["path"] = vrf_base_path + [v] + sp["path"]
                vrf_paths.append(sp)

    secret_paths = secret_paths + vrf_paths

    # Fixup for user SSH keys, that are nested under a tag node
    #ssh_key_base_path = {"base_path": ['system', 'login', 'user'], "secret_path": ["authentication", "encrypted-password"], "func": __anonymize_password},
    user_base_path = ['system', 'login', 'user']
    ssh_key_paths = []
    if config_tree.exists(user_base_path):
        for u in config_tree.list_nodes(user_base_path):
            kp = {"base_path": user_base_path + [u, "authentication", "public-keys"], "secret_path": ["key"], "func": __anonymize_key}
            ssh_key_paths.append(kp)

    secret_paths = secret_paths + ssh_key_paths

    # Fixup for OSPF passwords and keys that are nested under OSPF interfaces
    ospf_base_path = ["protocols", "ospf", "interface"]
    ospf_paths = []
    if config_tree.exists(ospf_base_path):
        for i in config_tree.list_nodes(ospf_base_path):
            # Plaintext password, there can be only one
            opp = {"path": ospf_base_path + [i, "authentication", "plaintext-password"], "func": __anonymize_password}
            md5kp = {"base_path": ospf_base_path + [i, "authentication", "md5", "key-id"], "secret_path": ["md5-key"], "func": __anonymize_password}
            ospf_paths.append(opp)
            ospf_paths.append(md5kp)

    secret_paths = secret_paths + ospf_paths

    return secret_paths

def __strip_private(ct, secret_paths):
    for sp in secret_paths:
        if "base_path" in sp:
            if ct.exists(sp["base_path"]):
                for n in ct.list_nodes(sp["base_path"]):
                    if ct.exists(sp["base_path"] + [n] + sp["secret_path"]):
                        secret = ct.return_value(sp["base_path"] + [n] + sp["secret_path"])
                        ct.set(sp["base_path"] + [n] + sp["secret_path"], value=sp["func"](secret))
        elif "path" in sp:
            if ct.exists(sp["path"]):
                secret = ct.return_value(sp["path"])
                ct.set(sp["path"], value=sp["func"](secret))
        else:
            raise ValueError("Malformed secret path dict, has neither base_path nor path in it ")

    return ct.to_string()

def strip_config_source(config_source):
    config_tree = vyos.configtree.ConfigTree(config_source)
    secret_paths = __prepare_secret_paths(config_tree, __secret_paths)
    stripped_config = __strip_private(config_tree, secret_paths)

    return stripped_config

def strip_config_tree(config_tree):
    secret_paths = __prepare_secret_paths(config_tree, __secret_paths)
    return __strip_private(config_tree, secret_paths)