Jenkins node (agent) on shared web server - what are the right permissions
A not very devopsy, but quite simple and effective solution with no security implications (no credential exchange or authentication required) would be to establish a file-based "message" exchange scheme, in a "mailbox" - a well-known filesystem location (setup once and owned by root) with 2 directories:


one owned by the jenkins user which creates request files containing deployment request information, one for each site
one owned by the apache group where each site dedicated users create their own response files containing request handling information for deployment requests for their site


When jenkins processing reaches a deployment stage for a particular site it creates a corresponding request file with the necessary information in it.

Each site user periodically (cron-driven, for example) checks for request files pertaining to their site, handle the request as appropriate, following their own site policies and provide status updates in the respective response files, which the jenkins user checks periodically.

When a request handling is completed the jenkins user removes the request file, signalling that it received the "message" and then the site user periodical job can remove the corresponding response file.

The names of the request and response files can be used to encode the particular site and request identification, so that the periodical checks don't have to fumble through multiple files.

The scheme can easily work across machines (if, for example, some of the sites are migrated to other servers) simply by placing the "mailbox" on a shared filesystem accessible from all those machines.

OK, an example, as requested. Just a basic skeleton, in python, hopefully self-documenting.

Prerequisites:

sudo mkdir -p /var/message_box/requests
sudo chown jenkins /var/message_box/requests
sudo chmod go-w /var/message_box/requests
sudo mkdir /var/message_box/responses
sudo chgrp apache /var/message_box/responses
sudo chmod g+w /var/message_box/responses


The mailbox.py file:

#!/usr/bin/python2.7 -u

import logging, os, re, getpass, sys, time, yaml

class Mailbox(object):

    base_dir = '/var/message_box'
    request_filename_format = '%s.%s.yaml'  # username.id.yaml

    def __init__(self):
        pass

    @property
    def request_dir(self):
        return os.path.join(self.base_dir, 'requests')

    @property
    def response_dir(self):
        return os.path.join(self.base_dir, 'responses')

    def msg_filename(self, user, request_id):
        return self.request_filename_format % (user, request_id)

    def request_file(self, user, request_id):
        return os.path.join(self.request_dir, self.msg_filename(user, request_id))

    def response_file(self, user, request_id):
        return os.path.join(self.response_dir, self.msg_filename(user, request_id))

    def create_msg_file(self, user, request_id, data, is_response=False):
        assert user and request_id and data and isinstance(data, dict)
        msg_file = self.response_file(user, request_id) if is_response else \
                   self.request_file(user, request_id)
        with open(msg_file, 'w') as fd:
            fd.write(yaml.dump(data))

    def msg_file_data(self, user, request_id, is_response=False):
        msg_file = self.response_file(user, request_id) if is_response else \
                   self.request_file(user, request_id)
        if os.path.exists(msg_file):
            with open(msg_file) as fd:
                data = yaml.load(fd.read())
            if data and isinstance(data, dict):  # expected data format
                return data
        return None

    def create_request(self, user, request_id, data):
        self.create_msg_file(user, request_id, data)
        logging.info('created request %s for %s' % (request_id, user))

    def create_response(self, request_id, status, response_data=None):
        assert status
        user = getpass.getuser()
        self.create_msg_file(user, request_id, {'status': status, 'data': response_data}, is_response=True)
        logging.info('created response %s with status %s for %s' % (request_id, status, user))


    def handle_requests(self):
        user = getpass.getuser()
        while True:  # keep handling requests indefinitely
            time.sleep(1)  # new request polling rate, in seconds
            for filename in os.listdir(self.request_dir):
                m = re.match('(.*)\.(.*)\.yaml', filename)
                if not m:  # not a valid request filename
                    continue
                [username, request_id] = m.groups()
                if username != user:  # not a request for this user
                    continue
                if os.path.exists(self.response_file(user, request_id)):
                    # request handling already started
                    # you may add here recovery code for request handling interrupted for whatever reason
                    continue
                msg_data = self.msg_file_data(user, request_id)
                if not msg_data:  # unexpected data format
                    continue

                logging.info('received request %s: %s' % (request_id, msg_data))

                # mark the request handling start
                self.create_response(request_id, 'in_progress')

                time.sleep(5)  # mock-up, replace with whatever request handling means

                # mark the request handling done
                self.create_response(request_id, 'done')  # you can add response data to the dict if needed

                logging.info('handled request %s, waiting for confirmation' % request_id)

                while True:  # wait for confirmation receipt before cleaning up
                    time.sleep(1)  # confirmation receipt polling rate, in seconds
                    if not os.path.exists(self.request_file(user, request_id)):
                        # the deletion of the request file is the confirmation receipt
                        logging.info('confirmation for request %s received, cleaning up' % request_id)
                        os.unlink(self.response_file(user, request_id))  # cleanup response file
                        break


    def execute_deployment(self, deployment_id, deployment_user, deployment_data):

        self.create_request(deployment_user, deployment_id, deployment_data)
        started = False
        while True:  # wait until it's done
            time.sleep(1)  # polling rate, in seconds
            msg_data = self.msg_file_data(deployment_user, deployment_id, is_response=True)
            if msg_data:
                status = msg_data.get('status')
                if status:
                    if not started:
                        logging.info('request %s handling started' % deployment_id)
                        started = True
                    if status == 'done':  # job completed
                        logging.info('request %s handling completed, cleaning up' % deployment_id)
                        # cleanup request file, which is the confirmation receipt
                        os.unlink(self.request_file(deployment_user, deployment_id))
                        break

def usage(err_msg, option_parser):
    if err_msg:
        logging.error('%s\n\n%s\n' % (err_msg, option_parser.format_help()))
    sys.exit(-1)


if __name__ == "__main__":
    import optparse

    logging.basicConfig(level=logging.DEBUG, format="%(levelname)5s  %(asctime)s %(filename)s:%(lineno)d] %(message)s")
    os.umask(022)

    p = optparse.OptionParser()
    p.add_option('-c', '--command', action='store', dest='command', choices=['deploy', 'handler'],
                 help='command/mode: &lt;deploy|handler&gt;, mandatory', default=None)
    p.add_option('-i', '--ID', action='store', dest='id',
                 help='deployment ID, mandatory for deploy command', default=None)
    p.add_option('-u', '--user', action='store', dest='user',
                 help='deployment user, mandatory for deploy command', default=None)
    p.add_option('-a', '--artifact', action='store', dest='artifact',
                 help='deployment artifact, mandatory for deploy command', default=None)

    opts, _ = p.parse_args()

    if not opts.command:
        usage('command is mandatory', p)

    mailbox = Mailbox()

    if opts.command == 'deploy':
        if not opts.id or not opts.user or not opts.artifact:
            usage('ID and user must be specified for deploy command', p)
        data = {'artifact': opts.artifact}
        mailbox.execute_deployment(opts.id, opts.user, data)

    elif opts.command == 'handler':
        mailbox.handle_requests()


The jenkins user driving the deployments, deployment info hacked to a string for this example - 'artifact' :

$ ./mailbox.py -c deploy -i 20 -u dancorn -a artifact
 INFO  2017-10-17 13:32:34,663 mailbox.py:49] created request 20 for dancorn
 INFO  2017-10-17 13:32:35,666 mailbox.py:109] request 20 handling started
 INFO  2017-10-17 13:32:40,678 mailbox.py:112] request 20 handling completed, cleaning up
$ ./mailbox.py -c deploy -i 123 -u dancorn -a artifact
 INFO  2017-10-17 13:33:32,359 mailbox.py:49] created request 123 for dancorn
 INFO  2017-10-17 13:33:33,362 mailbox.py:109] request 123 handling started
 INFO  2017-10-17 13:33:38,375 mailbox.py:112] request 123 handling completed, cleaning up
$


The apache users would launch the handler which in this example remains running (could be converted to a daemon, or a cron-driven approach):

$ ./mailbox.py -c handler
 INFO  2017-10-17 13:32:34,819 mailbox.py:77] received request 20: {'artifact': 'artifact'}
 INFO  2017-10-17 13:32:34,821 mailbox.py:55] created response 20 with status in_progress for dancorn
 INFO  2017-10-17 13:32:39,827 mailbox.py:55] created response 20 with status done for dancorn
 INFO  2017-10-17 13:32:39,827 mailbox.py:87] handled request 20, waiting for confirmation
 INFO  2017-10-17 13:32:40,828 mailbox.py:93] confirmation for request 20 received, cleaning up
 INFO  2017-10-17 13:33:32,888 mailbox.py:77] received request 123: {'artifact': 'artifact'}
 INFO  2017-10-17 13:33:32,889 mailbox.py:55] created response 123 with status in_progress for dancorn
 INFO  2017-10-17 13:33:37,891 mailbox.py:55] created response 123 with status done for dancorn
 INFO  2017-10-17 13:33:37,891 mailbox.py:87] handled request 123, waiting for confirmation
 INFO  2017-10-17 13:33:38,893 mailbox.py:93] confirmation for request 123 received, cleaning up