Deploying JumpServer on Kubernetes with DingTalk Alert Integration

This document provides a comprehensive guide for deploying JumpServer on Kubernetes, including Ingress configuration, Helm installation/upgrade/removal commands, and custom core component modifications for DingTalk alert integration. It covers Dockerfile customization, values.yaml configuration analysis, and Kubernetes deployment adjustments.

This article was published 489 days ago, some content may be outdated. If you have any questions, please leave a comment.

Ingress Configuration

Requires setting up ingress (see 13-K8s ingress-nginx Service Certificate) and enabling TCP forwarding (see 23-K8s ingress TCP Forwarding).

JumpServer Deployment

  • “Installation”
1
    helm install jms-k8s ./jumpserver-v4.6.0 -n jumpserver --create-namespace -f values.yaml
  • “Upgrade”
1
    helm upgrade jms-k8s ./jumpserver-v4.6.0 -n jumpserver --create-namespace -f values.yaml
  • “Uninstallation”
1
    helm -n jumpserver delete jms-k8s

Core Component Image

Modified core component to support DingTalk alerts.

1
2
3
4
docker pull docker.1ms.run/jumpserver/core:v4.6.0-ce

cd core
docker build -t jumpserver/core:v4.6.1-ce .
  • “Dockerfile”
1
2
3
    FROM docker.1ms.run/jumpserver/core:v4.6.0-ce

    COPY ./notifications.py /opt/jumpserver/apps/notifications/notifications.py
  • “notifications.py”
  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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import textwrap
import time
import traceback
from itertools import chain

from celery import shared_task
from django.utils.translation import gettext_lazy as _
from html2text import HTML2Text

from common.utils import lazyproperty
from common.utils.timezone import local_now
from notifications.backends import BACKEND
from settings.utils import get_login_title
from terminal.const import RiskLevelChoices
from users.models import User
from .models import SystemMsgSubscription, UserMsgSubscription

__all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message')

system_msgs = []
user_msgs = []


class MessageType(type):
    def __new__(cls, name, bases, attrs: dict):
        clz = type.__new__(cls, name, bases, attrs)

        if 'message_type_label' in attrs \
                and 'category' in attrs \
                and 'category_label' in attrs:
            message_type = clz.get_message_type()

            msg = {
                'message_type': message_type,
                'message_type_label': attrs['message_type_label'],
                'category': attrs['category'],
                'category_label': attrs['category_label'],
            }
            if issubclass(clz, SystemMessage):
                system_msgs.append(msg)
            elif issubclass(clz, UserMessage):
                user_msgs.append(msg)

        return clz


@shared_task(
    verbose_name=_('Publish the station message'),
    description=_(
        """This task needs to be executed for sending internal messages for system alerts, 
        work orders, and other notifications"""
    )
)
def publish_task(receive_user_ids, backends_msg_mapper):
    Message.send_msg(receive_user_ids, backends_msg_mapper)


def send_dingtalk_task(message):
    # Send to custom webhook
    import hmac, hashlib, base64, urllib.parse
    timestamp = str(round(time.time() * 1000))
    # JumpServer credentials
    access_token = '<access token>'
    secret = '<secret>'

    secret_enc = secret.encode('utf-8')
    string_to_sign = f'{timestamp}\n{secret}'
    string_to_sign_enc = string_to_sign.encode('utf-8')
    hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
    sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
    form_data = {
        "msgtype": "markdown",
        "markdown": {
            "title": "JumpServer Monitoring Alert",
            "text": message
        },
        "at": {
            "atMobiles": [
                "<mobile>"
            ],
            "isAtAll": False
        }
    }
    import requests
    try:
        res = requests.post(
            url=f'https://oapi.dingtalk.com/robot/send?access_token={access_token}&timestamp={timestamp}&sign={sign}',
            json=form_data)
    except Exception as e:
        with open('/opt/error.log', 'a+') as f:
            f.write(str(e))


class Message(metaclass=MessageType):
    """
    What's encapsulated here?
        Templates for different messages with a unified sending interface
        - publish: Implementation relates to message subscription table structure
        - send_msg
    """
    message_type_label: str
    category: str
    category_label: str
    text_msg_ignore_links = True
    command = None

    @classmethod
    def get_message_type(cls):
        return cls.__name__

    def publish_async(self):
        self.publish(is_async=True)

    @classmethod
    def gen_test_msg(cls):
        raise NotImplementedError

    def publish(self, is_async=False):
        raise NotImplementedError

    def get_backend_msg_mapper(self, backends):
        backends = set(backends)
        backends.add(BACKEND.SITE_MSG)  # Site message is mandatory
        backends_msg_mapper = {}
        for backend in backends:
            backend = BACKEND(backend)
            if not backend.is_enable:
                continue
            get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
            msg = get_msg_method()
            backends_msg_mapper[backend] = msg
        return backends_msg_mapper

    @staticmethod
    def send_msg(receive_user_ids, backends_msg_mapper):
        for backend, msg in backends_msg_mapper.items():
            try:
                backend = BACKEND(backend)
                client = backend.client()
                users = User.objects.filter(id__in=receive_user_ids).all()
                client.send_msg(users, **msg)
            except NotImplementedError:
                continue
            except:
                traceback.print_exc()

    @classmethod
    def send_test_msg(cls, ding=True, wecom=False):
        msg = cls.gen_test_msg()
        if not msg:
            return

        from users.models import User
        users = User.objects.filter(username='admin')
        backends = []
        if ding:
            backends.append(BACKEND.DINGTALK)
        if wecom:
            backends.append(BACKEND.WECOM)
        msg.send_msg(users, backends)

    @staticmethod
    def get_common_msg() -> dict:
        return {'subject': '', 'message': ''}

    def get_html_msg(self) -> dict:
        return self.get_common_msg()

    @staticmethod
    def html_to_markdown(html_msg):
        h = HTML2Text()
        h.body_width = 0
        content = html_msg['message']
        html_msg['message'] = h.handle(content)
        return html_msg

    def get_markdown_msg(self) -> dict:
        return self.html_to_markdown(self.get_html_msg())

    def get_text_msg(self) -> dict:
        h = HTML2Text()
        h.body_width = 90
        msg = self.get_html_msg()
        content = msg['message']
        h.ignore_links = self.text_msg_ignore_links
        msg['message'] = h.handle(content)
        return msg

    @lazyproperty
    def common_msg(self) -> dict:
        return self.get_common_msg()

    @lazyproperty
    def text_msg(self) -> dict:
        msg = self.get_text_msg()
        return msg

    @lazyproperty
    def markdown_msg(self):
        return self.get_markdown_msg()

    @lazyproperty
    def get_jumpserver_dingtalk_msg(self) -> str:
        date_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        msg = f"""## <font color="#FF0000">[JumpServer Monitoring Alert](https://jumpserver.example.com/)</font>πŸ”₯
### <font color="#FF0000">Alert Status</font>: {RiskLevelChoices.get_label(self.command['risk_level'])}
### <font color="#FF0000">Target Host</font>: {self.command['asset']}
### <font color="#FF0000">Target User</font>: {self.command['user']}
### <font color="#FF0000">Executed Command</font>: `{self.command['input']}`
### <font color="#FF0000">Alert Details</font>: User {self.command['user']} executed a risky command `{self.command['input']}` on {self.command['asset']}, please handle promptly!
### <font color="#FF0000">Trigger Time</font>: {date_str}"""
        return msg

    @lazyproperty
    def html_msg(self) -> dict:
        msg = self.get_html_msg()
        return msg

    @lazyproperty
    def html_msg_with_sign(self):
        msg = self.get_html_msg()
        msg['message'] = textwrap.dedent("""
        {}
        <small>
        <br />
        β€”
        <br />
        {}
        </small>
        """).format(msg['message'], self.signature)
        return msg

    @lazyproperty
    def text_msg_with_sign(self):
        msg = self.get_text_msg()
        msg['message'] = textwrap.dedent("""
        {}
        β€”
        {}
        """).format(msg['message'], self.signature)
        return msg

    @lazyproperty
    def signature(self):
        return get_login_title()

    # --------------------------------------------------------------
    # Support different messaging formats
    def get_dingtalk_msg(self) -> dict:
        # DingTalk restricts identical messages within a day, add timestamp suffix
        message = self.markdown_msg['message']
        time = local_now().strftime('%Y-%m-%d %H:%M:%S')
        suffix = '\n{}: {}'.format(_('Time'), time)

        return {
            'subject': self.markdown_msg['subject'],
            'message': message + suffix
        }

    def get_wecom_msg(self) -> dict:
        return self.markdown_msg

    def get_feishu_msg(self) -> dict:
        return self.markdown_msg

    def get_lark_msg(self) -> dict:
        return self.markdown_msg

    def get_email_msg(self) -> dict:
        return self.html_msg_with_sign

    def get_site_msg_msg(self) -> dict:
        return self.html_msg

    def get_slack_msg(self) -> dict:
        return self.markdown_msg

    def get_sms_msg(self) -> dict:
        return self.text_msg_with_sign

    @classmethod
    def get_all_sub_messages(cls):
        def get_subclasses(cls):
            """Returns all subclasses of argument, cls"""
            if issubclass(cls, type):
                subclasses = cls.__subclasses__(cls)
            else:
                subclasses = cls.__subclasses__()
            for subclass in subclasses:
                subclasses.extend(get_subclasses(subclass))
            return subclasses

        messages_cls = get_subclasses(cls)
        return messages_cls

    @classmethod
    def test_all_messages(cls, ding=True, wecom=False):
        messages_cls = cls.get_all_sub_messages()

        for _cls in messages_cls:
            try:
                _cls.send_test_msg(ding=ding, wecom=wecom)
            except NotImplementedError:
                continue


class SystemMessage(Message):
    def publish(self, is_async=False):
        subscription = SystemMsgSubscription.objects.get(
            message_type=self.get_message_type()
        )

        # Only send through enabled backends
        receive_backends = subscription.receive_backends
        receive_backends = BACKEND.filter_enable_backends(receive_backends)

        users = [
            *subscription.users.all(),
            *chain(*[g.users.all() for g in subscription.groups.all()])
        ]

        receive_user_ids = [u.id for u in users]
        backends_msg_mapper = self.get_backend_msg_mapper(receive_backends)
        if is_async:
            send_dingtalk_task(self.get_jumpserver_dingtalk_msg)
            # publish_task.delay(receive_user_ids, backends_msg_mapper)
        else:
            self.send_msg(receive_user_ids, backends_msg_mapper)

    @classmethod
    def post_insert_to_db(cls, subscription: SystemMsgSubscription):
        pass

    @classmethod
    def gen_test_msg(cls):
        raise NotImplementedError


class UserMessage(Message):
    user: User

    def __init__(self, user):
        self.user = user

    def publish(self, is_async=False):
        """
        Send messages through user-configured channels
        """
        sub = UserMsgSubscription.objects.get(user=self.user)
        backends_msg_mapper = self.get_backend_msg_mapper(sub.receive_backends)
        receive_user_ids = [self.user.id]
        if is_async:
            send_dingtalk_task(self.get_jumpserver_dingtalk_msg)
            # publish_task.delay(receive_user_ids, backends_msg_mapper)
        else:
            self.send_msg(receive_user_ids, backends_msg_mapper)

    @classmethod
    def get_test_user(cls):
        from users.models import User
        return User.objects.all().first()

    @classmethod
    def gen_test_msg(cls):
        raise NotImplementedError

Translated text here

values.yaml

 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
    replicaCount: 1
    
    image:
      repository: nginx
      pullPolicy: IfNotPresent
      tag: ""
    
    imagePullSecrets: []
    nameOverride: ""
    fullnameOverride: ""
    
    serviceAccount:
      create: true
      annotations: {}
      name: ""
    
    podAnnotations: {}
    
    podSecurityContext: {}
    
    securityContext: {}
    
    service:
      type: ClusterIP
      port: 80
    
    ingress:
      enabled: false
    
    resources: {}
    
    autoscaling:
      enabled: false
    
    nodeSelector: {}
    
    tolerations: []
    
    affinity: {}

Modify the Image Version of the Jumpserver Core Module

To modify the image version of the Jumpserver core module, follow these steps:

1
2
3
4
5
6
7
# Step 1: Edit the core module deployment
kubectl -n jumpserver edit deployments.apps jms-k8s-jumpserver-jms-core

# Step 2: Locate the image field in the container configuration
# Find the containers section and modify the image version to:
# - name: jms-core
#   image: jumpserver/core:v4.6.1-ce
Facing the sea with spring blossoms.
Built with Hugo
Theme Stack designed by Jimmy