Bu doküman, GNS3 üzerinde Cisco switch'lerin merkezi kullanıcı veritabanı üzerinden kimlik doğrulaması yapmasını, grup bazlı yetki almasını, komut bazlı TACACS+ authorization uygulanmasını ve TACACS accounting loglarının Elastic/Kibana üzerinde görüntülenmesini özetler.
Kurulumda Active Directory yerine açık kaynak bir LDAP sunucusu olan OpenLDAP kullanıldı. Keycloak, OpenLDAP ile user federation yapacak şekilde eklendi. SW1 üzerinde RADIUS + OpenLDAP login doğrulandı. SW2 üzerinde final durumda TACACS+ + OpenLDAP kullanıldı; admin, operator ve readonly roller için komut bazlı yetkilendirme yapıldı.
Not: IP adresleri, parolalar ve bazı isimler dokümantasyon amacıyla maskelenmiştir. Kendi ortamınıza göre değiştirmeniz gerekir.
1. Amaç
Amaç, Cisco switch'lere lokal kullanıcılarla değil, merkezi dizindeki hesaplarla giriş yapılmasını sağlamaktır.
Hedef akış:
SW1 kullanıcı login -> FreeRADIUS -> OpenLDAP -> Grup kontrolü -> Yetki seviyesi
SW2 kullanıcı login -> TACACS+ -> OpenLDAP -> Grup kontrolü -> Yetki seviyesi + komut yetkilendirme + accounting log
Final yetkilendirme modeli:
network-admins -> admin profile -> privilege 15, tüm shell komutları izinli
network-operators -> operator profile -> privilege 15, sınırlı operasyon komutları izinli
network-readonly -> readonly profile -> privilege 1, sadece show/exit/logout izinli
Bu yapı sayesinde çok sayıda kullanıcı için TACACS config dosyasına tek tek kullanıcı yazılmaz. Kullanıcı hangi LDAP grubundaysa o profile uygulanır.
2. Kullanılan bileşenler
| Bileşen | Rolü |
|---|---|
| Ubuntu | Docker servislerinin ve local GNS3 server'ın çalıştığı makine |
| GNS3 | Ağ topolojisinin oluşturulduğu lab ortamı |
| OpenLDAP | Kullanıcı ve grup veritabanı |
| phpLDAPadmin | OpenLDAP için web yönetim arayüzü |
| Keycloak | OpenLDAP user federation ve IAM/SSO katmanı |
| FreeRADIUS | SW1 için RADIUS AAA sunucusu |
| tac_plus-ng | SW2 için TACACS+ sunucusu |
| Logstash | TACACS accounting loglarını parse eder |
| Elasticsearch | Parse edilen logları indeksler |
| Kibana | Logları arama, filtreleme ve dashboard için kullanılır |
| Cisco vIOS-L2 | Lab switch imajı |
| GNS3 Cloud + TAP | Switch'leri Ubuntu üzerindeki AAA servislerine bağlayan sanal ağ |
3. Lab IP planı
Gerçek IP adresleri dokümantasyon amacıyla maskelenmiştir.
Ubuntu AAA Server : <AAA_SERVER_IP>
GNS3 TAP Interface : <TAP_GATEWAY_IP>/24
SW1 Management IP : <SW1_MGMT_IP>/24
SW2 Management IP : <SW2_MGMT_IP>/24
OpenLDAP Docker IP : <LDAP_CONTAINER_IP>
FreeRADIUS Docker IP : <RADIUS_CONTAINER_IP>
TACACS+ Docker IP : <TACACS_CONTAINER_IP>
Keycloak Docker IP : <KEYCLOAK_CONTAINER_IP>
Elasticsearch Docker IP : <ELASTICSEARCH_CONTAINER_IP>
Kibana Docker IP : <KIBANA_CONTAINER_IP>
Logstash Docker IP : <LOGSTASH_CONTAINER_IP>
Lab içinde kullanılan örnek TAP ağı:
TAP Gateway : <TAP_GATEWAY_IP>
SW1 : <SW1_MGMT_IP>
SW2 : <SW2_MGMT_IP>
4. Klasör yapısı
Ana dizin:
/opt/aaa-stack
Klasör yapısı:
/opt/aaa-stack
├── docker-compose.yml
├── freeradius
│ ├── Dockerfile
│ ├── clients.conf
│ ├── mods-enabled
│ │ └── ldap
│ └── sites-enabled
│ └── default
├── tacacs
│ ├── tac_plus-ng.cfg
│ └── logs
│ └── accounting.log
├── logstash
│ ├── pipeline
│ │ └── tacacs-accounting.conf
│ └── data
├── elasticsearch
│ └── data
└── keycloak
└── data
5. Docker Compose servisleri
Aşağıdaki compose örneği labda kullanılan servislerin genel yapısını gösterir. Parolalar, IP adresleri ve subnet değerleri maskelenmiştir.
services:
openldap:
image: osixia/openldap:1.5.0
container_name: openldap
hostname: openldap
environment:
LDAP_ORGANISATION: "Lab"
LDAP_DOMAIN: "lab.local"
LDAP_ADMIN_PASSWORD: "<LDAP_ADMIN_PASSWORD>"
LDAP_CONFIG_PASSWORD: "<LDAP_CONFIG_PASSWORD>"
LDAP_TLS: "false"
volumes:
- openldap_data:/var/lib/ldap
- openldap_config:/etc/ldap/slapd.d
ports:
- "389:389"
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <LDAP_CONTAINER_IP>
phpldapadmin:
image: osixia/phpldapadmin:0.9.0
container_name: phpldapadmin
environment:
PHPLDAPADMIN_LDAP_HOSTS: openldap
PHPLDAPADMIN_HTTPS: "false"
ports:
- "8080:80"
depends_on:
- openldap
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <PHPLDAPADMIN_CONTAINER_IP>
freeradius:
build:
context: ./freeradius
container_name: freeradius
depends_on:
- openldap
ports:
- "1812:1812/udp"
- "1813:1813/udp"
volumes:
- ./freeradius/clients.conf:/etc/freeradius/3.0/clients.conf:ro
- ./freeradius/mods-enabled/ldap:/etc/freeradius/3.0/mods-enabled/ldap:ro
- ./freeradius/sites-enabled/default:/etc/freeradius/3.0/sites-enabled/default:ro
command: freeradius -X
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <RADIUS_CONTAINER_IP>
tacacs:
image: christianbecker/tac_plus-ng:latest
container_name: tacacs
depends_on:
- openldap
ports:
- "49:49/tcp"
volumes:
- ./tacacs/tac_plus-ng.cfg:/etc/tac_plus-ng.cfg:ro
- ./tacacs/logs:/var/log/tac_plus-ng
command: tac_plus-ng /etc/tac_plus-ng.cfg
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <TACACS_CONTAINER_IP>
keycloak:
image: quay.io/keycloak/keycloak:26.6.1
container_name: keycloak
command:
- start-dev
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: <KEYCLOAK_ADMIN_USER>
KC_BOOTSTRAP_ADMIN_PASSWORD: <KEYCLOAK_ADMIN_PASSWORD>
ports:
- "8081:8080"
volumes:
- ./keycloak/data:/opt/keycloak/data
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <KEYCLOAK_CONTAINER_IP>
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
container_name: elasticsearch
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
ports:
- "9200:9200"
volumes:
- ./elasticsearch/data:/usr/share/elasticsearch/data
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <ELASTICSEARCH_CONTAINER_IP>
kibana:
image: docker.elastic.co/kibana/kibana:8.15.3
container_name: kibana
depends_on:
- elasticsearch
environment:
ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
ports:
- "5601:5601"
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <KIBANA_CONTAINER_IP>
logstash:
image: docker.elastic.co/logstash/logstash:8.15.3
container_name: logstash-tacacs
depends_on:
- elasticsearch
- tacacs
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline:ro
- ./logstash/data:/usr/share/logstash/data
- ./tacacs/logs:/var/log/tacacs:ro
restart: unless-stopped
networks:
aaa_net:
ipv4_address: <LOGSTASH_CONTAINER_IP>
networks:
aaa_net:
driver: bridge
ipam:
config:
- subnet: <DOCKER_AAA_SUBNET>
volumes:
openldap_data:
openldap_config:
Not: Keycloak bu labda
start-devile çalıştırıldı. Production ortamda dev mode yerine kalıcı veritabanı, TLS, reverse proxy/hostname ve HA planı yapılmalıdır.
6. OpenLDAP kullanıcı ve grup verisi
Bu labda ilk kullanıcı ve grup kayıtları phpLDAPadmin web arayüzü üzerinden oluşturuldu.
phpLDAPadmin arayüzüne tarayıcıdan erişildi:
http://<AAA_SERVER_IP>:8080
Login bilgileri:
Login DN : cn=admin,dc=lab,dc=local
Password : <LDAP_ADMIN_PASSWORD>
OpenLDAP içinde temel dizin yapısı:
dc=lab,dc=local
├── ou=people
└── ou=groups
Başlangıç kullanıcıları:
uid=netadmin,ou=people,dc=lab,dc=local
uid=readonly,ou=people,dc=lab,dc=local
Sonradan PBİK formatına uygun 6 haneli kullanıcılar da eklendi:
uid=100001,ou=people,dc=lab,dc=local
uid=100002,ou=people,dc=lab,dc=local
Bu kullanıcı adları switch login username olarak kullanılır:
Username: 100001
Username: 100002
Gruplar:
cn=network-admins,ou=groups,dc=lab,dc=local
cn=network-operators,ou=groups,dc=lab,dc=local
cn=network-readonly,ou=groups,dc=lab,dc=local
Grup üyelikleri member attribute'u ile tanımlandı:
network-admins:
member: uid=netadmin,ou=people,dc=lab,dc=local
network-operators:
member: uid=100001,ou=people,dc=lab,dc=local
network-readonly:
member: uid=readonly,ou=people,dc=lab,dc=local
member: uid=100002,ou=people,dc=lab,dc=local
Özet yetkilendirme:
netadmin -> network-admins -> admin profile
100001 -> network-operators -> operator profile
readonly -> network-readonly -> readonly profile
100002 -> network-readonly -> readonly profile
Not:
groupOfNamesobjectClass'i en az birmemberattribute'u ister. Bu yüzden gruptaki son kullanıcı silinmeye çalışılırsa LDAP object class violation hatası alınabilir.
7. Keycloak ve OpenLDAP federation
Keycloak bu labda TACACS+ server'ın yerine geçmedi. Switch tarafında cihazlar TACACS+ konuştuğu için enforcement noktası tac_plus-ng olarak kaldı.
Keycloak'un bu labdaki rolü:
OpenLDAP kullanıcı ve gruplarını görmek
OpenLDAP ile user federation yapmak
Writable modda Keycloak UI üzerinden OpenLDAP'a kullanıcı/grup/membership yazabilmek
İleride SSO/OIDC/SAML/MFA gibi IAM ihtiyaçları için zemin sağlamak
Gerçek AAA flow şu şekildedir:
SW2 -> TACACS+ -> tac_plus-ng -> OpenLDAP
Keycloak doğrudan switch login isteğini karşılamaz. Cisco switch Keycloak token okumaz; switch TACACS+ konuşur.
7.1 Keycloak erişimi
Keycloak host üzerinde şu porttan yayınlandı:
http://<AAA_SERVER_IP>:8081
Admin login:
Username: <KEYCLOAK_ADMIN_USER>
Password: <KEYCLOAK_ADMIN_PASSWORD>
Realm:
network-aaa
7.2 LDAP User Federation ayarı
Keycloak içinde:
User federation -> Add provider -> ldap
Örnek ayarlar:
UI display name : openldap
Vendor : Other
Connection URL : ldap://openldap:389
Enable StartTLS : Off
Bind type : simple
Bind DN : cn=admin,dc=lab,dc=local
Bind credentials : <LDAP_ADMIN_PASSWORD>
Users DN : ou=people,dc=lab,dc=local
Username LDAP attr : uid
RDN LDAP attr : uid
UUID LDAP attr : entryUUID
Search scope : Subtree
Import users : On
Edit mode : WRITABLE
Sync registrations : On
Kullanıcı object class ayarı writable kullanım için sade tutuldu:
inetOrgPerson, organizationalPerson, person
Bu nedenle TACACS LDAP filter da inetOrgPerson ile uyumlu olacak şekilde güncellendi.
7.3 LDAP group mapper
Keycloak LDAP provider altında group mapper eklendi:
Mapper type : group-ldap-mapper
LDAP Groups DN : ou=groups,dc=lab,dc=local
Group Name LDAP Attribute : cn
Group Object Classes : groupOfNames
Membership LDAP Attribute : member
Membership Attribute Type : DN
Membership User LDAP Attribute: uid
User Groups Retrieve Strategy : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE
Mode : LDAP_ONLY
Bu ayar ile Keycloak'ta kullanıcıyı bir gruba eklemek OpenLDAP'taki groupOfNames/member attribute'unu günceller.
7.4 Keycloak'tan oluşturulan kullanıcıların OpenLDAP'a yazılması
Keycloak üzerinden PBİK formatında kullanıcı oluşturuldu:
Username: 100002
OpenLDAP üzerinde kontrol:
docker exec -it openldap ldapsearch \
-x \
-H ldap://localhost:389 \
-D "cn=admin,dc=lab,dc=local" \
-w '<LDAP_ADMIN_PASSWORD>' \
-b "ou=people,dc=lab,dc=local" \
"(uid=100002)"
Örnek beklenen çıktı:
dn: uid=100002,ou=people,dc=lab,dc=local
uid: 100002
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
cn: 100002
sn: 100002
userPassword: <PASSWORD_HASH_OR_VALUE>
Keycloak üzerinden 100002 kullanıcısı network-readonly grubuna eklendi. LDAP grup üyeliği şu komutla doğrulandı:
docker exec -it openldap ldapsearch \
-x \
-H ldap://localhost:389 \
-D "cn=admin,dc=lab,dc=local" \
-w '<LDAP_ADMIN_PASSWORD>' \
-b "ou=groups,dc=lab,dc=local" \
"(cn=network-readonly)" member
Beklenen:
member: uid=100002,ou=people,dc=lab,dc=local
Bu test ile şu doğrulandı:
Keycloak UI üzerinden kullanıcı oluşturuldu.
Kullanıcı OpenLDAP'a yazıldı.
Keycloak üzerinden grup üyeliği verildi.
Bu üyelik OpenLDAP groupOfNames/member attribute'una yazıldı.
TACACS+ aynı OpenLDAP grup üyeliğini okuyarak yetki verdi.
7.5 Keycloak token konusu
Keycloak üzerinden kullanıcı token'ı alınabilir ve token içinde roller görülebilir. Ancak Cisco IOS switch bu token'ı kullanmaz.
Önemli ayrım:
Keycloak token -> OIDC/SAML/web uygulamaları için anlamlıdır.
Cisco switch -> TACACS+ konuşur, OIDC token okumaz.
Bu nedenle bu labda Keycloak token'ı üzerinden doğrudan command authorization yapılmadı. Token/role bazlı doğrudan TACACS karar modeli istenirse ek bir MAVIS adapter, policy service veya bunu native destekleyen kurumsal AAA ürünü gerekir.
8. FreeRADIUS Dockerfile
Dosya:
/opt/aaa-stack/freeradius/Dockerfile
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y freeradius freeradius-ldap ldap-utils && \
rm -rf /var/lib/apt/lists/*
EXPOSE 1812/udp 1813/udp
CMD ["freeradius", "-X"]
9. FreeRADIUS client tanımları
Dosya:
/opt/aaa-stack/freeradius/clients.conf
client localhost {
ipaddr = 127.0.0.1
secret = <RADIUS_SHARED_SECRET>
require_message_authenticator = no
nas_type = other
}
client docker_bridge {
ipaddr = <DOCKER_AAA_SUBNET>
secret = <RADIUS_SHARED_SECRET>
require_message_authenticator = no
nas_type = other
}
client gns3_tap_lab {
ipaddr = <GNS3_TAP_SUBNET>
secret = <RADIUS_SHARED_SECRET>
require_message_authenticator = no
nas_type = cisco
}
10. FreeRADIUS LDAP modülü
Dosya:
/opt/aaa-stack/freeradius/mods-enabled/ldap
ldap {
server = "openldap"
port = 389
identity = "cn=admin,dc=lab,dc=local"
password = "<LDAP_ADMIN_PASSWORD>"
base_dn = "dc=lab,dc=local"
user {
base_dn = "ou=people,dc=lab,dc=local"
filter = "(uid=%{%{Stripped-User-Name}:-%{User-Name}})"
}
group {
base_dn = "ou=groups,dc=lab,dc=local"
filter = "(objectClass=groupOfNames)"
membership_filter = "(&(objectClass=groupOfNames)(member=%{control:Ldap-UserDn}))"
name_attribute = cn
}
options {
chase_referrals = yes
rebind = yes
res_timeout = 10
srv_timelimit = 3
net_timeout = 1
idle = 60
probes = 3
interval = 3
}
tls {
start_tls = no
}
}
11. FreeRADIUS default site
Dosya:
/opt/aaa-stack/freeradius/sites-enabled/default
server default {
listen {
type = auth
ipaddr = *
port = 1812
}
listen {
type = acct
ipaddr = *
port = 1813
}
authorize {
ldap
if (ok) {
update control {
Auth-Type := LDAP
}
}
}
authenticate {
Auth-Type LDAP {
ldap
}
}
post-auth {
if (&LDAP-Group == "network-admins") {
update reply {
Service-Type := Administrative-User
Cisco-AVPair := "shell:priv-lvl=15"
Reply-Message := "LDAP RADIUS Admin Login OK"
}
}
elsif (&LDAP-Group == "network-readonly") {
update reply {
Service-Type := NAS-Prompt-User
Cisco-AVPair := "shell:priv-lvl=1"
Reply-Message := "LDAP RADIUS Readonly Login OK"
}
}
else {
reject
}
}
}
12. TACACS+ config
TACACS+ tarafında tac_plus-ng kullanıldı. Bu servis, MAVIS LDAP backend üzerinden OpenLDAP'a bağlanır.
Final model:
network-admins -> admin_profile
network-operators -> operator_profile
network-readonly -> readonly_profile
Dosya:
/opt/aaa-stack/tacacs/tac_plus-ng.cfg
id = spawnd {
listen { port = 49 }
spawn {
instances min = 1
instances max = 16
}
}
id = tac_plus-ng {
log acctlog {
destination = /var/log/tac_plus-ng/accounting.log
}
accounting log = acctlog
mavis module = external {
setenv LDAP_HOSTS = "ldap://openldap:389"
setenv LDAP_BASE = "ou=people,dc=lab,dc=local"
setenv LDAP_BASE_GROUP = "ou=groups,dc=lab,dc=local"
setenv LDAP_SCOPE = sub
setenv LDAP_SCOPE_GROUP = sub
setenv LDAP_FILTER = "(&(objectClass=inetOrgPerson)(uid=%s))"
setenv LDAP_FILTER_GROUP = "(&(objectClass=groupOfNames)(member=%s))"
setenv LDAP_USER = "cn=admin,dc=lab,dc=local"
setenv LDAP_PASSWD = "<LDAP_ADMIN_PASSWORD>"
exec = /usr/local/lib/mavis/mavis_tacplus-ng_ldap.pl
}
user backend = mavis
login backend = mavis
pap backend = mavis
device gns3_tap_switches {
address = <GNS3_TAP_SUBNET>
key = <TACACS_SHARED_SECRET>
}
profile admin_profile {
script {
if (service == shell) {
if (cmd == "") {
set priv-lvl = 15
permit
}
permit
}
}
}
profile operator_profile {
script {
if (service == shell) {
if (cmd == "") {
set priv-lvl = 15
permit
}
if (cmd =~ /^show(\s|$)/) {
permit
}
if (cmd =~ /^configure\s+terminal(\s|$)/) {
permit
}
if (cmd =~ /^interface\s+/) {
permit
}
if (cmd =~ /^ip\s+address\s+/) {
permit
}
if (cmd =~ /^no\s+shutdown(\s|$)/) {
permit
}
if (cmd =~ /^exit(\s|$)/) {
permit
}
if (cmd =~ /^end(\s|$)/) {
permit
}
if (cmd =~ /^logout(\s|$)/) {
permit
}
deny
}
}
}
profile readonly_profile {
script {
if (service == shell) {
if (cmd == "") {
set priv-lvl = 1
permit
}
if (cmd =~ /^show(\s|$)/) {
permit
}
if (cmd =~ /^exit(\s|$)/) {
permit
}
if (cmd =~ /^logout(\s|$)/) {
permit
}
deny
}
}
}
ruleset {
rule admin_rule {
enabled = yes
script {
if (memberof =~ /^cn=network-admins,ou=groups,dc=lab,dc=local$/) {
profile = admin_profile
permit
}
}
}
rule operator_rule {
enabled = yes
script {
if (memberof =~ /^cn=network-operators,ou=groups,dc=lab,dc=local$/) {
profile = operator_profile
permit
}
}
}
rule readonly_rule {
enabled = yes
script {
if (memberof =~ /^cn=network-readonly,ou=groups,dc=lab,dc=local$/) {
profile = readonly_profile
permit
}
}
}
rule deny_all {
enabled = yes
script {
deny
}
}
}
}
Syntax test:
docker run --rm -it \
--network aaa-stack_aaa_net \
-v /opt/aaa-stack/tacacs/tac_plus-ng.cfg:/etc/tac_plus-ng.cfg:ro \
-v /opt/aaa-stack/tacacs/logs:/var/log/tac_plus-ng \
christianbecker/tac_plus-ng:latest \
tac_plus-ng /etc/tac_plus-ng.cfg
Restart:
cd /opt/aaa-stack
docker compose restart tacacs
13. Servisleri başlatma
cd /opt/aaa-stack
docker compose up -d --build
Kontrol:
docker ps
Beklenen container'lar:
openldap
phpldapadmin
freeradius
tacacs
keycloak
elasticsearch
kibana
logstash-tacacs
Port kontrolü:
sudo ss -lntup | egrep ':389|:8080|:8081|:1812|:1813|:49|:9200|:5601'
Beklenen portlar:
389/tcp OpenLDAP
8080/tcp phpLDAPadmin
8081/tcp Keycloak
1812/udp RADIUS authentication
1813/udp RADIUS accounting
49/tcp TACACS+
9200/tcp Elasticsearch
5601/tcp Kibana
14. GNS3 tarafında yapılanlar
Local Ubuntu üzerindeki GNS3 server kullanıldı.
TAP interface oluşturuldu:
sudo ip tuntap add dev tap-gns3 mode tap user $USER
sudo ip addr add <TAP_GATEWAY_IP>/24 dev tap-gns3
sudo ip link set tap-gns3 up
GNS3 Cloud node içinde tap-gns3 seçildi.
Neden ESW1 kullanıldı?
Cloud üzerindeki TAP port tek bağlantı kabul ettiği için, birden fazla switch'i aynı TAP ağına bağlamak amacıyla araya GNS3 built-in Ethernet switch eklendi.
14.1 TAP interface'i kalıcı hale getirme
TAP interface reboot sonrasında kaybolmasın diye systemd service oluşturuldu.
Script:
sudo tee /usr/local/sbin/gns3-tap-up.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -e
TAP_NAME="tap-gns3"
TAP_USER="<LINUX_USER>"
TAP_IP="<TAP_GATEWAY_IP>/24"
if ! ip link show "$TAP_NAME" >/dev/null 2>&1; then
ip tuntap add dev "$TAP_NAME" mode tap user "$TAP_USER"
fi
ip addr flush dev "$TAP_NAME" || true
ip addr add "$TAP_IP" dev "$TAP_NAME"
ip link set "$TAP_NAME" up
EOF
sudo chmod +x /usr/local/sbin/gns3-tap-up.sh
Stop script:
sudo tee /usr/local/sbin/gns3-tap-down.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -e
TAP_NAME="tap-gns3"
if ip link show "$TAP_NAME" >/dev/null 2>&1; then
ip link set "$TAP_NAME" down || true
ip tuntap del dev "$TAP_NAME" mode tap || true
fi
EOF
sudo chmod +x /usr/local/sbin/gns3-tap-down.sh
Systemd service:
sudo tee /etc/systemd/system/gns3-tap.service > /dev/null <<'EOF'
[Unit]
Description=Create persistent TAP interface for GNS3 lab
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/gns3-tap-up.sh
ExecStop=/usr/local/sbin/gns3-tap-down.sh
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable gns3-tap.service
sudo systemctl start gns3-tap.service
Kontrol:
systemctl status gns3-tap.service --no-pager
ip -br addr show tap-gns3
Beklenen:
tap-gns3 UP <TAP_GATEWAY_IP>/24
15. SW1 temel IP config
enable
conf t
hostname SW1
interface vlan 1
ip address <SW1_MGMT_IP> 255.255.255.0
no shutdown
exit
ip default-gateway <TAP_GATEWAY_IP>
end
write memory
Test:
show ip interface brief
ping <TAP_GATEWAY_IP>
16. SW2 temel IP config
enable
conf t
hostname SW2
interface vlan 1
ip address <SW2_MGMT_IP> 255.255.255.0
no shutdown
exit
ip default-gateway <TAP_GATEWAY_IP>
end
write memory
Test:
show ip interface brief
ping <TAP_GATEWAY_IP>
17. SW1 için Cisco RADIUS AAA config
SW1 final durumda RADIUS örneği olarak bırakıldı.
conf t
aaa new-model
radius server AAA-RADIUS
address ipv4 <TAP_GATEWAY_IP> auth-port 1812 acct-port 1813
key <RADIUS_SHARED_SECRET>
aaa group server radius RADIUS-GRP
server name AAA-RADIUS
aaa authentication login default group RADIUS-GRP local
aaa authorization exec default group RADIUS-GRP local
username localadmin privilege 15 secret <LOCAL_FALLBACK_PASSWORD>
line vty 0 15
login authentication default
authorization exec default
transport input telnet ssh
end
write memory
Kontrol:
show aaa servers
show running-config | section radius
show running-config | section aaa
SW1 login testi:
telnet <SW1_MGMT_IP>
Admin kullanıcı:
Username: netadmin
Password: <NETADMIN_PASSWORD>
Beklenen:
SW1#
Current privilege level is 15
Readonly kullanıcı:
Username: readonly
Password: <READONLY_PASSWORD>
Beklenen:
SW1>
Current privilege level is 1
18. SW2 için Cisco TACACS+ AAA config
SW2 final durumda TACACS+ kullanacak şekilde bırakıldı.
Lab imajı eski IOS syntax'ını kabul ettiği için TACACS server şu formatla tanımlandı:
conf t
aaa new-model
tacacs-server host <TAP_GATEWAY_IP> key <TACACS_SHARED_SECRET>
aaa group server tacacs+ TACACS-GRP
server <TAP_GATEWAY_IP>
username localadmin privilege 15 secret <LOCAL_FALLBACK_PASSWORD>
aaa authentication login default group TACACS-GRP local
aaa authorization exec default group TACACS-GRP local
aaa authorization commands 1 default group TACACS-GRP local
aaa authorization commands 15 default group TACACS-GRP local
aaa authorization config-commands
aaa accounting exec default start-stop group TACACS-GRP
aaa accounting commands 1 default start-stop group TACACS-GRP
aaa accounting commands 15 default start-stop group TACACS-GRP
line vty 0 15
login authentication default
authorization exec default
transport input ssh
end
write memory
Komutların anlamı:
aaa authentication login default group TACACS-GRP local
Login authentication için önce TACACS+, sonra local fallback.
aaa authorization exec default group TACACS-GRP local
Kullanıcı login sonrası shell/exec açabilir mi ve privilege seviyesi ne olacak?
aaa authorization commands 1/15 default group TACACS-GRP local
Privilege 1 ve 15 komutları için command authorization yap.
aaa authorization config-commands
Config mode içindeki komutları da authorization kapsamına al.
aaa accounting exec default start-stop group TACACS-GRP
Oturum başlangıç/bitiş accounting kaydı üret.
aaa accounting commands 1/15 default start-stop group TACACS-GRP
Privilege 1 ve 15 komutlarını accounting'e gönder.
Not:
localfallback labda kilitlenmemek için kullanıldı. Production ortamda fallback davranışı güvenlik politikasına göre ayrıca değerlendirilmelidir.
TACACS+ test komutları:
test aaa group TACACS-GRP netadmin <NETADMIN_PASSWORD> legacy
test aaa group TACACS-GRP netadmin <WRONG_PASSWORD> legacy
Beklenen:
Doğru şifre -> User was successfully authenticated
Yanlış şifre -> User authentication request was rejected by server
19. SW2 SSH config
Telnet yerine SSH kullanılması için SW2 üzerinde RSA key ve SSH v2 açıldı.
conf t
ip domain-name lab.local
crypto key generate rsa modulus 2048
ip ssh version 2
ip ssh time-out 60
ip ssh authentication-retries 3
line vty 0 15
login authentication default
authorization exec default
transport input ssh
end
write memory
Kontrol:
show ip ssh
show running-config | section line vty
Beklenen:
SSH Enabled - version 2.0
transport input ssh
Labdaki vIOS-L2 imajı eski SSH algoritmaları sunduğu için Ubuntu'dan bağlantıda legacy OpenSSH seçenekleri kullanıldı:
ssh \
-oKexAlgorithms=+diffie-hellman-group14-sha1 \
-oHostKeyAlgorithms=+ssh-rsa \
-oPubkeyAcceptedAlgorithms=+ssh-rsa \
<USERNAME>@<SW2_MGMT_IP>
Örnek:
ssh \
-oKexAlgorithms=+diffie-hellman-group14-sha1 \
-oHostKeyAlgorithms=+ssh-rsa \
-oPubkeyAcceptedAlgorithms=+ssh-rsa \
100001@<SW2_MGMT_IP>
Production ortamda güncel IOS-XE veya güncel SSH algoritmaları destekleyen cihazlarda normal SSH komutu yeterli olabilir.
20. SW2 TACACS+ komut bazlı yetkilendirme testleri
Admin test
Kullanıcı:
netadmin -> network-admins
Beklenen:
SW2#show privilege
Current privilege level is 15
SW2#write memory
# çalışır
Operator test
Kullanıcı:
100001 -> network-operators
Beklenen:
SW2#show privilege
Current privilege level is 15
SW2#conf t
SW2(config)#interface vlan 1
SW2(config-if)#no shutdown
# çalışır
SW2#write memory
Command authorization failed.
SW2#reload
Command authorization failed.
Operator kullanıcının privilege seviyesi 15 olsa bile tüm komutları çalıştıramaz. Çünkü command authorization açık olduğu için switch her yetkilendirilen komutta TACACS+ server'a sorar.
Readonly test
Kullanıcı:
100002 -> network-readonly
Beklenen:
SW2>show privilege
Current privilege level is 1
SW2>show ip interface brief
# çalışır
SW2>conf t
Command authorization failed.
21. TACACS accounting loglama
SW2 üzerinde accounting açıldığı için login/logout ve komut kayıtları TACACS+ server tarafında tutulur. Bu loglar switch diskine yazılmaz.
TACACS config içinde accounting log dosyası tanımı:
log acctlog {
destination = /var/log/tac_plus-ng/accounting.log
}
accounting log = acctlog
Docker volume sayesinde container içindeki log dosyası host üzerinde şu path'e yazılır:
/opt/aaa-stack/tacacs/logs/accounting.log
Canlı izleme:
tail -f /opt/aaa-stack/tacacs/logs/accounting.log
Örnek log satırları:
<TIMESTAMP> <SWITCH_IP> 100001 tty2 <SOURCE_IP> start shell
<TIMESTAMP> <SWITCH_IP> 100001 tty2 <SOURCE_IP> stop shell show privilege <cr>
<TIMESTAMP> <SWITCH_IP> 100001 tty2 <SOURCE_IP> stop shell write memory <cr>
Alanların anlamı:
timestamp -> olay zamanı
switch IP -> observer.ip, işlem yapılan switch
username -> user.name
tty -> terminal/session hattı
source IP -> switch'e bağlanan kaynak IP
start/stop -> accounting action
shell -> servis tipi
command -> çalıştırılan komut
source.ip labda genelde TAP gateway IP olarak görünür. Çünkü SSH bağlantıları Ubuntu host/TAP üzerinden yapılmaktadır. Gerçek ortamda bu alan admin bilgisayarı veya jump server IP'si olabilir.
22. Logstash pipeline
Logstash, TACACS accounting log dosyasını okuyup alanlara ayırır ve Elasticsearch'e gönderir.
Dosya:
/opt/aaa-stack/logstash/pipeline/tacacs-accounting.conf
input {
file {
path => "/var/log/tacacs/accounting.log"
start_position => "beginning"
sincedb_path => "/usr/share/logstash/data/tacacs-accounting.sincedb"
mode => "tail"
codec => "plain"
}
}
filter {
if [message] =~ /^\s*$/ {
drop { }
}
mutate {
replace => {
"[event][original]" => "%{message}"
}
add_field => {
"[event][dataset]" => "tacacs.accounting"
"[event][module]" => "tacacs"
}
}
grok {
match => {
"message" => "^%{YEAR:[tacacs][year]}-%{MONTHNUM:[tacacs][month]}-%{MONTHDAY:[tacacs][day]} %{TIME:[tacacs][time]} %{ISO8601_TIMEZONE:[tacacs][timezone]} %{IP:[observer][ip]}\t%{DATA:[user][name]}\t%{DATA:[tacacs][tty]}\t%{IP:[source][ip]}\t%{WORD:[event][action]}\t%{WORD:[service][name]}(?:\t%{GREEDYDATA:[tacacs][command]})?$"
}
tag_on_failure => [ "tacacs_parse_failed" ]
}
mutate {
add_field => {
"[tacacs][timestamp_raw]" => "%{[tacacs][year]}-%{[tacacs][month]}-%{[tacacs][day]} %{[tacacs][time]} %{[tacacs][timezone]}"
}
strip => [
"[tacacs][command]",
"[user][name]",
"[observer][ip]",
"[source][ip]",
"[event][action]",
"[service][name]"
]
}
date {
match => [ "[tacacs][timestamp_raw]", "yyyy-MM-dd HH:mm:ss Z" ]
target => "@timestamp"
tag_on_failure => [ "tacacs_dateparse_failed" ]
}
if ![tacacs][command] or [tacacs][command] == "" {
mutate {
add_field => {
"[tacacs][command_type]" => "session"
}
}
} else {
mutate {
add_field => {
"[tacacs][command_type]" => "command"
}
}
ruby {
code => '
cmd = event.get("[tacacs][command]")
if cmd && cmd.strip != ""
event.set("[tacacs][command_prefix]", cmd.strip.split(/\s+/)[0])
end
'
}
}
if [tacacs][command_prefix] == "no" {
mutate {
add_field => {
"[event][risk_score]" => "50"
"[tacacs][command_category]" => "negative_config"
}
}
} else if [tacacs][command_prefix] == "configure" {
mutate {
add_field => {
"[event][risk_score]" => "40"
"[tacacs][command_category]" => "configuration_mode"
}
}
} else if [tacacs][command_prefix] == "write" {
mutate {
add_field => {
"[event][risk_score]" => "30"
"[tacacs][command_category]" => "save_config"
}
}
} else if [tacacs][command_prefix] == "show" {
mutate {
add_field => {
"[event][risk_score]" => "10"
"[tacacs][command_category]" => "read_only"
}
}
}
mutate {
remove_field => [
"[tacacs][year]",
"[tacacs][month]",
"[tacacs][day]",
"[tacacs][time]",
"[tacacs][timezone]"
]
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "tacacs-accounting-%{+YYYY.MM.dd}"
}
stdout {
codec => rubydebug
}
}
Logstash restart:
cd /opt/aaa-stack
docker compose restart logstash
Index kontrolü:
curl "http://localhost:9200/_cat/indices/tacacs-accounting-*?v"
23. Kibana kullanımı
Kibana erişimi:
http://<AAA_SERVER_IP>:5601
Data view:
Name : tacacs-accounting
Index pattern : tacacs-accounting-*
Timestamp : @timestamp
Discover ekranında önerilen kolonlar:
@timestamp
observer.ip
source.ip
user.name
event.action
tacacs.command_type
tacacs.command_prefix
tacacs.command_category
tacacs.command
tacacs.tty
Örnek filtreler:
user.name : "100001"
observer.ip : "<SW2_MGMT_IP>"
tacacs.command_type : "command"
tacacs.command_prefix : "no"
tacacs.command : "write memory <cr>"
user.name : "100001" and tacacs.command_prefix : "configure"
user.name : "100002" and tacacs.command_type : "command"
Bu şekilde şu sorular yanıtlanabilir:
Kim switch'e login oldu?
Kim hangi switch üzerinde işlem yaptı?
Kim hangi komutu çalıştırdı?
Kim configure terminal yaptı?
Kim no ile başlayan komut çalıştırdı?
Kim write memory denedi?
Readonly kullanıcı hangi komutları çalıştırdı?
Operator kullanıcı hangi komutlarda deny aldı?
Not: TACACS accounting bazı IOS davranışlarında deny edilen her komutu loglamayabilir. Çalıştırılan/işlenen komutlar accounting tarafında daha net görünür. Deny eventlerinin ayrıca saklanması gerekiyorsa switch syslog veya TACACS authorization debug/log seçenekleri ayrıca değerlendirilmelidir.
24. Log retention ve disk konusu
TACACS accounting logları switch diskine yazılmaz. Loglar AAA sunucusu tarafında tutulur:
/opt/aaa-stack/tacacs/logs/accounting.log
Bu yüzden switch diskini şişirmez. Disk büyümesi Ubuntu/Elastic tarafında olur.
Raw accounting log için logrotate örneği:
sudo tee /etc/logrotate.d/tacacs-accounting > /dev/null <<'EOF'
/opt/aaa-stack/tacacs/logs/accounting.log {
daily
rotate 30
compress
missingok
notifempty
copytruncate
}
EOF
Test:
sudo logrotate -d /etc/logrotate.d/tacacs-accounting
Elasticsearch tarafında production için ILM/retention policy planlanmalıdır.
25. 5651 ve log bütünlüğü notu
Bu yapı TACACS+ command accounting loglarını merkezi olarak saklar ve Kibana üzerinden aranabilir hale getirir.
Ancak tek başına tam bir 5651 uyumlu loglama sistemi olarak değerlendirilmemelidir. 5651 benzeri uyumluluk ihtiyaçları için genellikle şu ek konular gerekir:
NTP ile doğru zaman senkronizasyonu
Log bütünlüğü için hash/imza/zaman damgası
Yetkisiz değişikliğe karşı koruma
Retention policy
Erişim kontrolü
Gerekirse WORM/değiştirilemez saklama
Merkezi SIEM veya 5651 uyumlu loglama ürünü
Bu labdaki yapı network cihaz yönetimi açısından şu audit sorularını karşılar:
Kim login oldu?
Hangi cihazda işlem yaptı?
Hangi komutu çalıştırdı?
Ne zaman çalıştırdı?
26. Güncel final flow
Final durumda lab akışı şöyledir:
Keycloak UI
|
| LDAP federation / writable user & group management
v
OpenLDAP
|
| LDAP user + group membership
v
TACACS+ Server - tac_plus-ng
|
| permit / deny / priv-lvl
v
Cisco SW2
|
| TACACS accounting
v
accounting.log
|
v
Logstash -> Elasticsearch -> Kibana
Switch login/authorization akışı:
1. Kullanıcı SSH ile SW2'ye bağlanır.
2. SW2, username/password bilgisini TACACS+ server'a gönderir.
3. tac_plus-ng, MAVIS LDAP backend ile OpenLDAP'a sorar.
4. OpenLDAP kullanıcıyı doğrular.
5. tac_plus-ng kullanıcının LDAP grup üyeliğini okur.
6. Grup üyeliğine göre admin/operator/readonly profile seçilir.
7. Kullanıcı komut çalıştırdığında SW2 yine TACACS+ server'a command authorization sorar.
8. tac_plus-ng ilgili profile göre permit/deny döner.
9. Accounting logları Logstash üzerinden Elasticsearch'e gider.
Keycloak'un rolü:
Keycloak doğrudan switch authentication server'ı değildir.
Switch Keycloak token okumaz.
Switch TACACS+ konuşur.
Keycloak bu labda OpenLDAP kullanıcı/grup yönetimi ve IAM/federation katmanıdır.
Bu nedenle doğru teknik anlatım:
Keycloak, OpenLDAP/AD ile entegre edildi. Kullanıcı ve grup yönetimi Keycloak üzerinden yapılabiliyor ve LDAP'a yazılıyor. TACACS+ ise switch CLI enforcement noktası olarak kalıyor. tac_plus-ng, LDAP grup üyeliklerine göre admin/operator/readonly profillerini uyguluyor. Tüm login ve komut kayıtları TACACS accounting ile Elastic/Kibana'ya aktarılıyor.
27. Sonuç
Bu lab ile Cisco switch login ve yönetim işlemleri merkezi hale getirildi.
Final mimari:
SW1 -> FreeRADIUS -> OpenLDAP -> LDAP Group -> Cisco privilege level
SW2 -> TACACS+ -> OpenLDAP -> LDAP Group -> Cisco privilege level + command authorization + accounting
Keycloak -> OpenLDAP federation/writable user-group management
TACACS accounting -> Logstash -> Elasticsearch -> Kibana
RADIUS tarafı merkezi login ve privilege atamasını göstermek için kullanıldı.
TACACS+ tarafı network cihaz yönetiminde daha ileri seviye kontrol sağlamak için kullanıldı:
Kullanıcı doğrulama
Exec authorization
Komut bazlı authorization
Komut accounting
Merkezi loglama
Keycloak bu yapıda kullanıcı/grup yönetimini ve IAM katmanını sağlar. Switch tarafındaki gerçek CLI enforcement ise TACACS+ ile yapılır.
Bu yapı gerçek ortamda Active Directory ile benzer mantıkta uygulanabilir:
Cisco Switch -> TACACS+/RADIUS -> AD/OpenLDAP -> Grup üyeliği -> Yetki profili
Top comments (0)